0%

DMA 重映射

iommu 作用

  1. 安全性, 防止恶意访问
    In the absence of an IOMMU, a device driver must program devices with Physical Addresses, which implies that DMA from a device could be used to access any memory, such as privileged memory,and cause malicious or unintended corruptions. This may be caused by hardware bugs, devicedriver bugs, or by malicious software/hardware. 013

在没有IOMMU的情况下,设备驱动程序必须用物理地址对设备进行编程,这意味着来自设备的DMA可以被用来访问任何内存,如特权内存,并造成恶意或意外的损坏。这可能是由硬件错误、设备驱动程序错误或恶意软件/硬件造成的。

  1. 使传统的32位外设可以访问超过4G的内存区间, 不再需要软件做 bounce buffers, 提高性能
    Legacy 32-bit devices cannot access the memory above 4 GiB. 013
    The integration of the IOMMU,through its address remapping capability, offers a simple mechanism for the DMA to directly accessany address in the system 013
    Without an IOMMU, the OS must resort to copying data through buffers (also known as bounce buffers) allocated in memory below 4GiB. 013

  2. The IOMMU can be useful as it permits to allocate large regions of memory without the need to becontiguous in physical memory 013
    可以使用连续物理内存

中断重映射

MSI 重映射

To handle MSIs from a device controlled by a guest OS, the hypervisor configures an IOMMU toredirect those MSIs to a guest interrupt file in an IMSIC (see Figure 3) or to a memory-residentinterrupt file. The IOMMU is responsible to use the MSI address-translation data structures suppliedby the hypervisor to perform the MSI redirection. Because every interrupt file, real or virtual,occupies a naturally aligned 4-KiB page of address space, the required address translation is from avirtual (guest) page address to a physical page address, 015

hypervisor配置了一个IOMMU,将这些guest 的MSI (GPA) 重定向到IMSIC中的guest interrupt file (HPA)

利用 iommu 重定向能力 使的 guest msi的GPA地址访问直接映射为 HPA的msi的地址访问, 从而让guest 可以直接读写物理msi mmio, 实现中断重映射能力, 中断直通给vcpu.

MSI 的地址重映射(GPA->HPA)是hypervisor 帮guest os做的 iommu 映射

Device-Directory-Table DDT

base format dc

extended format dc

Non-leaf DDT entry

A valid (V==1) non-leaf DDT entry provides PPN of the next level DDT.

Leaf DDT entry

The leaf DDT page is indexed by DDI[0] and holds the device-context (DC). 024
In base-format the DC is 32-bytes. In extended-format the DC is 64-bytes. 024

下面是base format dc DDI[0]的描述

Translation control (tc)

Translation control (tc) 025

The PDTV is expected to be set to 1 when DC is associated with a device that supports multiple process contexts and thus generates a valid process_id with its memory accesses. For PCIe, for example, if the request has a PASID then the PASID is used as the process_id. 027

tc.PDTV = 0 时, fsc 为 iostap/iovstap, 即1-stage 映射基址
tc.PDTV = 1 时, fsc 为 pdtp (associated with a device that supports multiple process contexts)

iohgatp

IO hypervisor guest address translation and protection (iohgatp) 027

The iohgatp field holds the PPN of the root G-stage page table and a virtual machine identified by aguest soft-context ID (GSCID) 027

The root page table as determined by iohgatp.PPN is 16 KiB and must be aligned to a 16-KiBboundary. If the root page table is not aligned to 16 KiB as required, then all entries in that G-stageroot page table appear to an IOMMU as UNSPECIFIED and any address an IOMMU may compute anduse for accessing an entry in the root page table is also UNSPECIFIED. 027

iohgatp.mode

The G-stage page table format and MODE encoding follow the format defined by the privileged specification. 027
mode-> Bare or 同hgatp的mode

fsc

First-Stage context (fsc) 027

tc.PDTV = 0 & iohgatp.mode = Bare 该域表示 iosatp;
tc.PDTV = 0 & iohgatp.mode != Bare 该域表示 iovstatp


格式同satp

tc.PDTV = 1 该域表示 PDTP

When PDTV is 1, the fsc field holds the process-directory table pointer (pdtp). 028

When the device supports multiple process contexts, selected by the process_id, the PDT is used to determine the S/VS-stage page table and associated PSCID for virtual address translation and protection. 028

The pdtp field holds the PPN of the root PDT and the MODE field that determines the number of levels of the PDT.

PDT 和 PSCID 结合找出对应的页表基址

pdtp.mode

^a5d0fe

ta

Translation attributes (ta) 028

The PSCID field of ta provides the process soft-context ID that identifies the address-space of the process. PSCID facilitates address-translation fences on a per-address-space basis.
The PSCID field in ta is used as the address-space ID if PDTV is 0 and the iosatp/iovsatp MODE field is not Bare. 029

tc.pdtv = 0 & iosatp/iovsatp.mode != Bare 时, ta.PSCID 表示ASID

PDT Process-Directory-Table

Non-leaf PDT entry

V == 1 表示为 非leaf 节点

Leaf PDT entry

First-Stage context (fsc)

First-Stage context (fsc) 031

The software assigned process soft-context ID (PSCID) is used as the address space ID (ASID) for the process identified by the S/VS-stage page table. 032

pdtv = 1
The pdtp field holds the PPN of the root PDT and the MODE field that determines the number of levels of the PDT.

pdtp.mode 确定了使用几级页表

Translation attributes (ta)

PC is valid if the V bit is 1; If it is 0, all other bits in PC are don’t care and may be freely used bysoftware. 031

MSI page table

MSI page table pointer (msiptp) 029
DC.msiptp


An MSI page table is a flat array of MSI page table entries (MSI PTEs) 032
Msi page table 只有一级页表

地址A匹配

a write to guest physical address A is recognized as an MSI to a virtual interrupt file 029
(A >> 12) & ~msi_addr_mask = (msi_addr_pattern & ~msi_addr_mask)
认为是msi 地址

Each MSI PTE may specify either the address of a real guest interruptfile that substitutes for the targeted virtual interrupt file (下图1), or a memory-residentinterrupt file in which to store incoming MSIs for the virtual interrupt file(下图2) . 032

MSI PTE write-through mode

also called msipte

If V = 1 and the custom-use bit C = 0, then bit 2 of the first doubleword is field W (Write-through). If W = 1, the MSI PTE specifies write-through mode for incoming MSIs, and if W = 0, it specifies MRIFmode. 033

When an MSI PTE has fields V = 1, C = 0, and W = 1 (write-through mode), the PTE’s complete
format is:

An MSI PTE in write-through mode allows a hypervisor to route an MSI intended for a virtual interrupt file to go instead to a guest interrupt file of a real IMSIC in the machine. 033

Memory-Mapped Register

所有的寄存器在下面的这个表格中
Table 10. IOMMU Memory-mapped register layout 065

其中比较重要的有ddtp

ddtp

Device-directory-table pointer (ddtp) 069

iommu_mode

IOMMU capabilities

IOMMU capabilities (capabilities) 066

翻译过程

Process to translate an IOVA

Process to translate an IOVA 038

整个翻译过程还是比较复杂的, 需要根据多个字段判断是DC/PC/MSI table
最终将pte确定后, 根据iohgatp.mode 确定是2-stage 还是1-stage 的SPA/GPA翻译.

用device_id 来确定DDT的叶子节点DDTE 是必须的, 无论最终是 locate DC/PC/MSI, 都需要DDTE的解读.

Process to translate addresses of MSIs

Process to translate addresses of MSIs 043

  1. 省略了MRIF 模式, 这部分比较复杂, 一般也用不到, 等用到的时候再另行分析
  2. 非MRIF模式, 即msipte.W=1时, 提取的IMSIC的interrupt file 的index no为 extract(iova>>12, DC.msi_addr_mask)
    ◦ x = a b c d e f g h
    ◦ y = 1 0 1 0 0 1 1 0
    ◦ r = acfg
  3. 最终配置的地址为 msipte.PPN << 12 | iova[11:0], 其中 msipte = DC.msiptp.PPN << 12 | interrupt_file_no * 16;

Process to locate the Device-context

Process to locate the Device-context 040
use device_id, 该流程是和前面的 translate iova 结合来看的

The device context is located using the hardware provided unique device_id. The supported device_id width may be up to 24-bit. 019

  • capabilities.MSI_FLAT == 0
    DDI[0] -> device_id[6:0]
    DDI[1] -> device_id[15:7]
    DDI[2] -> device_id[23:16]
  • capabilities.MSI_FLAT == 1
    DDI[0] -> device_id[5:0]
    DDI[1] -> device_id[14:6]
    DDI[2] -> device_id[23:15]

如capabilities.MSI_FLAT == 0 时, base dc walk 图

Process to locate the Process-context

Process to locate the Process-context 042
use process_id
The hardware identities associated with transaction - the device_id and if applicable the process_id. The IOMMU uses the hardware identities to retrieve the context information to perform the requested address translations 017

process_id 是20 bit, 由pdtp.MODE 确定几级页表

PDI[0] = process_id[7:0]
PDI[1] = process_id[16:8]
PDI[2] = process_id[19:17]

Queue Interface

  1. A command-queue (CQ) used by software to queue commands to the IOMMU.
  2. A fault/event queue (FQ) used by IOMMU to bring faults and events to software attention.
  3. A page-request queue (PQ) used by IOMMU to report “Page Request” messages received from
    PCIe devices. This queue is supported if the IOMMU supports PCIe defined Page Request
    Interface

CQ

cmdq

The PPN of the base of this in-memory queue and the size of the queue is configured into a
memory-mapped register called command-queue base (cqb).

The tail of the command-queue resides in a software controlled read/write memory-mapped
register called command-queue tail (cqt).

The head of the command-queue resides in a read-only memory-mapped IOMMU controlled
register called command-queue head (cqh)

If cqh == cqt, the command-queue is empty.
If cqt == (cqh - 1) the command-queue is full.

IOMMU Page-Table cache invalidation commands

IOTINVAL.VMA ensures that previous stores made to the S/VS-stage page tables by the harts are
observed by the IOMMU before all subsequent implicit reads from IOMMU to the corresponding
S/VS-stage page tables.

IOTINVAL.GVMA ensures that previous stores made to the G-stage page tables are observed before all
subsequent implicit reads from IOMMU to the corresponding G-stage page tables. Setting PSCV to 1
with IOTINVAL.GVMA is illegal.

IOMMU Command-queue Fence commands

A IOFENCE.C command guarantees that all previous commands fetched from the CQ have been
completed and committed

FQ

faultq

Fault/Event queue is an in-memory queue data structure used to report events and faults raised
when processing transactions.

The PPN of the base of this in-memory queue and the size of the queue is configured into a
memory-mapped register called fault-queue base (fqb).

The tail of the fault-queue resides in a IOMMU controlled read-only memory-mapped register called
fqt.

The fqh is an index into the next fault record that SW should process next.


CAUSE:


PQ

priq or Page-Request-Queue

Page-request queue is an in-memory queue data structure used to report PCIe ATS “Page Request”
and “Stop Marker” messages to software.

The base PPN of this in-memory queue and the size of the queue is configured into a memory-mapped register called page-request queue base (pqb).

The tail of the queue resides in a IOMMU controlled read-only memory-mapped register called pqt.

The head of the queue resides in a software controlled read/write memory-mapped register called
pqh.

If pqh == pqt, the page-request queue is empty.
If pqt == (pqh - 1) the page-request queue is full.

PCIe 相关

ATS & PRI
Address Translate Service
Page Request Interface

PCIe 设备可以发出缺页请求,iommu硬件在解析到缺页请求后可以直接将缺页请求写入 PRI queue (PQ), 软件在建立好页表后,可以通过CMD queue 发送 PRI response 给 PCIe 设备。具体的 ATS 和 PRI 的实现是硬件相关的
软件控制 DMA 发起后,设备先发起 ATC 请求,从 IOMMU 请求该 va 对应的 pa,如果 IOMMU 里已经有 va 到 pa 的映射,那么设备可以得到 pa,然后设备再用 pa 发起一次内存访问,该访问将直接访问对应 pa 地址,不在 IOMMU 做地址翻译;
如果 IOMMU 没有 va 到 pa 的映射, 那么设备得到这个消息后会继续向 IOMMU 发 PRI 请求,设备得到从 IOMMU 来的 PRI response 后发送内存访问请求,该请求就可以在 IOMMU 中翻译得到 pa, 最终访问到物理内存。

从上述链路的角度看, 设备端相当于做了tlb的事情.


ATS机制想要解决的问题(优势):

  1. 能够分担主机(CPU)侧的查表压力,特别是大带宽、大拓扑下的IO数据流,CPU侧的IOMMU的查表将会成为性能瓶颈,而ATS机制正好可以提供将这些查表压力卸载到不同的设备中,对整个系统实现“who consume it, who pay for it”。
  2. 决定查表性能的好坏的一个最为关键的点是TLB的预测。而像传统PCIe IO数据流,在CPU侧集中式做IOMMU的查表,对于其TLB的预测(prefetch)是很困难,极为不友好的。因为很多不同workload的IO流汇聚到一点,对于TLB的预测的冲击很大,流之间的干扰很大,很难做出准确的预测,从而TLB的命中率始终做不到太高,从而影响IO性能。而ATS的机制恰好提供了一个TLB的预测卸载到源头去的机制,让用户(设备)自己根据自己自身的业务流来设计自己的预测策略,而且用户彼此之间的预测模型不会受到彼此的影响,从而大大提高了用户自己的预测的准确性。抽象来看,这时候的设备更像是CPU核,直接根据自身跑的workload来预测本地的TLB,从而提升预测性能,进而提升整系统的预测性能。

RERI 规范在 SoC 中增加了 RAS 功能,通过内存映射寄存器接口提供错误报告的标准机制,提供记录检测到的错误(包括其严重性、性质和位置)的功能,并配置了向 RAS 处理程序组件发送错误信号的方法。
RAS 处理程序可以使用此信息来确定合适的恢复操作,这些操作可能包括

  • 终止(例如,终止一个进程等)
  • 重新启动部分或全部系统等,以便从错误中恢复。
  • 此外,该规范应支持软件发起的 RAS 处理程序的错误记录、报告和测试。
  • 最后,该规范应提供实现错误处理的最大灵活性,并应与其他标准(如 PCIe、CXL 等)定义的 RAS 框架共存。

错误级别

  • CE Corrected error.
  • UDE Uncorrected deferred error.
  • UUE Uncorrected urgent error.

寄存器

比较重要的 csr:

  • Address register (addr_i)
    The addr_i WARL register reports the address associated with the detected error
  • Information register (info_i)
    The info_i WARL register provides additional information about the error when status_i.iv is 1
  • Status register (status_i)
    The status_i is a read-write WARL register that reports errors detected by the hardware unit
  • Error bank information (bank_info)

    inst_id 字段标识组件的一个包或至少一个芯片内的唯一实例;在整个系统中最好是独一无二的。系统的供应商将 inst_id 定义为组件的唯一标识符。返回值为0表示该字段未实现。
    n_err_recs 字段表示错误记录的数量。
  • Control register (control_i)
    control_i 是一个读/写 WARL 寄存器,用于控制错误库中相应错误记录的错误报告。
    The ces, udes, and uues are WARL fields used to enable signaling of CE, UDE, and UUE respectively When they are logged

华为 hisi 1620

APEI

UEFI
ACPI Platform Error Interfaces

Provides a standard way to convey error info From Firmware to OS

BERT

Boot Error Record Table
Record fatal errors, then report it in the second boot
记录启动过程中的关键错误信息, 在下一次启动时报告错误.

  • 在 OS 未接管平台的控制权限之前 firmware(如 BIOS 或者 UEFI)检测到错误,导致系统无法继续启动,可以通过 BIOS/FIRMWARE 将这种类型的错误写入到特定的存储位置。这样一来,在下一次的正常启动过程中,OS 可以通过特定的方法将之前保存的错误读取出来分析并处理。
  • 系统运行过程中 firmware 检测到了致命错误,以至于 firmware 决定不通知 OS 而是直接重启(如 CPU 风扇突然坏了,瞬间过热,如果不立刻重启会烧毁 CPU),在重启前 firmware 可以记录下相关的错误信息以便之后分析出错原因

注:只有 BIOS/FIRMWARE 才有能力对 BERT 执行写入操作;对于 OS 而言,BERT 仅仅是一个只读的表。BERT 出现的意义在于希望采用一种统一的接口来记录特定类型的硬件错误(主要是一些致命的),从而简化 BIOS/FIRMWARE 和 OS 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ ApeiEntryPoint(ImageHandle, SystemTable)
\ -|+ if SetupData.EnRasSupport
"通过协议的 GUID 查找对应的协议, 查找acpi 表格协议"
\ -+ gBS->LocateProtocol(&gEfiAcpiTableProtocolGuid, NULL, &mAcpiTableProtocol);
"查找 ACPI 标准数据表协议, 操作系统和驱动程序可以获取系统硬件配置信息,进行系统初始化和配置,以及支持电源管理等功能。"
| -+ gBS->LocateProtocol(&gEfiAcpiSdtProtocolGuid, NULL, &mAcpiSdtProtocol);
| -+ gBS->AllocatePool (EfiReservedMemoryType, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), (VOID**)&mApeiTrustedfirmwareData) "分配内存池"
| - gBS->SetMem (mApeiTrustedfirmwareData, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), 0)); "memset 0"
| -+ OemInitBertTable (ImageHandle);
\ - BERT_CONTEXT Context;
| -+ BertHeaderCreator (&Context, BOOT_ERROR_REGION_SIZE); "Bert 表分配内存"
\ - Context->BertHeader = AllocateZeroPool (sizeof (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER));
| - Context->Block = AllocateReservedZeroPool (ErrorBlockSize);
"构造header 包含上图的 OEMID OEM_table_id creator_id等如 EFI_ACPI_ARM_OEM_REVISION EFI_ACPI_ARM_CREATOR_ID"
| - *Context->BertHeader = (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER) {
ARM_ACPI_HEADER(
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_SIGNATURE,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_REVISION
),
| - ErrorBlockInitial (Context.Block, EFI_ACPI_6_2_ERROR_SEVERITY_NONE);
| -+ BertSetAcpiTable (&Context); "初始化 Bert Table"
"ACPI 表格是存储着系统硬件配置信息和固件与操作系统通信信息的数据结构。在 UEFI 中,
有时需要向 UEFI 固件中添加自定义的 ACPI 表格,
以便在操作系统启动时提供特定的系统配置信息,或支持特定的硬件功能"
\ -+ mAcpiTableProtocol->InstallAcpiTable ( "通过Acpi 表格协议安装 Bert 表"
mAcpiTableProtocol,
Bert,
Bert->Header.Length,
&AcpiTableHandle);

linux driver

drivers/acpi/apei/bert.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-+ bert_init
\ -+ acpi_get_table(ACPI_SIG_BERT, 0, (struct acpi_table_header **)&bert_tab); "获取bert table"
| - region_len = bert_tab->region_length; "获取 bert_table length"
| - apei_resources_init(&bert_resources); "初始化 apei_resources 结构体"
"向bert_resources 下添加IOMEM 类型的资源,以便在发生错误时能够正确地处理这些资源。 "
| - apei_resources_add(&bert_resources, bert_tab->address, region_len, true);
| -+ apei_resources_request(&bert_resources, "APEI BERT");
| -+ boot_error_region = ioremap_cache(bert_tab->address, region_len);
| -|+ if boot_error_region
\ -+ bert_print_all(boot_error_region, region_len);
\ - "Error records from previous boot"
| -+ cper_estatus_print(KERN_INFO HW_ERR, estatus);
\ - " %s event severity: %s", severity "打印log错误级别"
| -+ foreach section
\ -+ cper_estatus_print_section(newpfx, gdata, sec_no)
\ - severity = gdata->error_severity;
| - printk("%s""Error %d, type: %s\n", pfx, sec_no, cper_severity_str(severity));
| - printk("%s""fru_id: %pUl\n", pfx, gdata->fru_id); "fru_id fru_text等, 见上图的表结构"
| - printk("%s""fru_text: %.20s\n", pfx, gdata->fru_text);
...

ERST

Error Record Serialization Table
Provides details necessary to communicate with on-board persistent storage for error recording

提供必要的详细信息, 协同存储错误记录

ERST 本质上是一个用来永久存储错误的抽象接口软件可以通过 ERST 表将各种错误信息保存到 ERST 中,再由 ERST 写入到可用于永久存储的物理介质中。ERST 并没有一个严格的定义来界定什么是“错误”,换言之,软件可以保存任何信息到 ERST 中,只要软件认为是有意义,有价值的信息就可以.
物理介质未必一定是 flash 或 NVRAM,可以是网络存储或者其他。

ERST 的主要作用就是用来存储各种硬件或者平台相关的错误,错误类型包括:

  • Corrected Error(CE)
  • Uncorrected Recoverable Error(UCR)
  • Uncorrected Non-Recoverable Error,或者说 Fatal Error。

换言之,只要是软件可以记录的错误,都可以将其保存到 ERST 当中。加上之前谈到的 BERT 表,这样一来,无论系统运行在哪个阶段,当出现硬件或平台相关的错误时,通过 APEI 接口,都有办法将错误保存下来。这样一来就可以在之后通过适当的方法将错误读取出来进行分析,从而加快定位产生错误的原因并加以解决。

EINJ

Error Injection Table
Provides a generic Interface which OSPM can inject hardware Errors to the platform without requiring platform Specific software.

提供通用接口方便 os 向硬件注入错误.

GHES of HEST

Generic Hardware Error Source - GHES
Hardware Error Source Table - HEST

HOW to get trigger: Notification Structure
WHERE are the error records:

Error Status Address
(GAS : Generic Address Structure)

HOW to release records’ mem:
Read Ack Register

在 HEST 中定义了很多硬件相关的错误源和错误类型。定义这些硬件错误源的目的在于标准化软硬件错误接口的实现。有了 HEST,当发生特定类型的硬件错误,如 PCI-E 设备产生了一个 Uncorrected Recoverable 类型的错误时,BIOS/FIRMWARE 有统一的方法更新特定的寄存器和内部状态,软件有统一的方法去处理和解析错误。HEST 中定义了很多硬件错误源,如 MCE、PCI-E、GHES 等等。

其中最为特殊也是最为重要的硬件错误源类型就是 GHES (Generic Hardware Error Source)。GHES 是一个通用硬件错误源,换言之,任何类型的硬件错误都可以使用 GHES 来定义,而无需使用之前提到的特定硬件错误源,如内存控制器错误等。

当前无论是软件还是 BIOS/FIRMWARE 的实现,基本上都是只使用 GHES 来实现 HEST 的功能,至于其他特定的硬件错误源,基本上都没有使用(PCI-E AER 的部分代码检测了 PCI-E 类型的硬件错误源)。

在 FFM (Firmware-First handling) 使能的情况下,一般而言,所有 CE 类型的错误通过 SCI 中断报告给 OS,然后 OS 在 HEST/GHES 中查表,检测并处理可能的硬件错误;所有 UC 和 Fatal 类型的错误通过 NMI 报告给 OS,然后 OS 在 NMI 的 handler 中查表,检测并处理可能的硬件错误。这些规定并不是硬性要求的,平台设计者完全可以根据需要使用 NMI 来处理所有的错误类型,包括 CE, UC 和 Fatal 类型的错误,也可以只使用 NMI 来处理 UC 和 Fatal 类型的错误,而使用轮询的方式来处理 CE 类型的错误。

linux kernel ghes 处理

Ghes_probe 函数中,根据 HEST 表中传递的检测错误类型,查看相关 kernel 配置选项是否支持。包括 arm 相关的 SEA 错误,NMI,本地中断。
调用 ghes_new 函数,初始化 struct ghes 结构。初始化 ghes 结构,映射表中 Error Status Address。为存放错误信息数据申请内存。

根据上报错误方式,注册不同的处理流程,包括如下:
(1)轮询方式,根据表中传递的 poll_interval 时间,创建定时器。在定时器处理函数 ghes_poll_func 中,调用 ghes_proc。在这个函数中:
A、读取 GHES 结构中传递的 Error Status Address。首先将读到的 struct acpi_hest_generic_status 结构拷贝到前面申请的内存中,检测相关错误信息长度是否合法。然后将后面的错误信息拷贝。
B、如上报错误的严重级别大于 GHES_SEV_PANIC,则将错误信息打印,清除错误状态以及记录错误信息的内存块。然后进入 kernel panic。
C、调用 ghes_do_proc 函数处理错误。这个函数中,获取错误数据块中各 section 中 section_type 以及 error_severity。同时判断 fru_id 和 fru_text 字段是否有效。 1>如为内存相关错误,以下为错误类型分类
* @ HW_EVENT_ERR_CORRECTED: Corrected Error - 表示检测到 ECC 纠正的错误
* @ HW_EVENT_ERR_UNCORRECTED : 表示 ECC 无法纠正的错误,但不是致命的错误 (可能是在未使用的内存区域,或者内存控制器可以从中恢复,例如,通过重新尝试操作)
* @ HW_EVENT_ERR_DEFERRED: Deferred Error - 表示处理不紧急的不可纠正的错误。这可能是由于硬件数据中毒,系统可以继续操作,直到中毒的数据被消耗。也可以采取主动的措施,例如,offlining 页面等。
* @ HW_EVENT_ERR_FATAL: 致命错误-无法恢复的不可更正错误。
* @ HW_EVENT_ERR_INFO: 规范定义了第四种类型的错误: 信息日志。
首先获取 struct cper_sec_mem_err 数据块。调用
Ghes_edac_report_mem_error 函数,这个函数中,将错误信息中包括错误类型,错误地址,内存颗粒,错误内存位置填充到错误报告 buffer 中(struct edac_raw_error_desc)。
错误类型如下:

把错误写到 ftrace 的一个跟踪项中,最后调用 edac_raw_mc_handle_error
分别通过 edac_ce_error 处理 ECC 类型的错误以及调用 edac_ue_error 处理其他错误。
最后调用 ghes_handle_memory_failure
如果
- 错误级别为可修复级别且 CPER_SEC_ERROR_THRESHOLD_EXCEEDED 置位(表示内核中止使用这个资源)
- 错误是可恢复的类型。满足上述条件之一则调用 memory_failure_queue 函数。这个函数在检测到页面的硬件内存损坏时由硬件错误处理程序调用。它调度错误页面的恢复,包括删除页面,杀死进程等。

如为 pcie aer 错误
PCIe AER 错误需要发送到 AER 驱动程序进行报告和恢复。GHES 的严重程度与以下 AER 严重程度相对应,并需要进行以下处理:
- GHES_SEV_CORRECTABLE -> AER_CORRECTABLE — 需要由 AER 驱动报告,但不需要恢复。
- GHES_SEV_RECOVERABLE -> AER_NONFATAL
- GHES_SEV_RECOVERABLE && CPER_SEC_RESET -> AER_FATAL
这两种情况都需要 AER 驱动报告和恢复。
GHES_SEV_PANIC 不会进行这种处理,因为内核必须将进入 panic 状态。

(2) 如果为外部中断,则通过 GHES 中传递的中断号,申请中断处理函数,在中断处理函数 ghes_irq_func 调用 ghes_proc。下面的处理流程和上面轮询一致。
(3) 同样的如果为 SCI 中断,调用 notifier_call 回调函数 ghes_notify_hed,遍历 ghes_hed 链表,分别执行 ghes_proc 函数,处理错误
(4) 如果为 NMI 中断,处理错误级别大于 GHES_SEV_PANIC,则直接 kernel panic。否则如配置 CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG,则调到工作队列处理函数 ghes_proc_in_irq 中。执行相当于下半部的处理过程。最终调用 ghes_do_proc 函数执行上述相关错误的处理。

RAS

RAS(Reliability、Availability and Serviceability),即可靠性、可用性、可维护性。

以下是 RAS 的三个主要目标:

  1. 提升系统可运行时间。
    RAS 技术可以提升服务器的可靠性,一般通过测量平均故障时间(MTTF)、年崩溃率(ACR)或年服务率(ASR)来度量。一个可靠的系统将保持运行更长的时间,因此更加可用。

  2. 减少非计划停机时间。
    当非计划停机出现时,可以通过测量平均修复时间 MTTR 来度量服务器的可维护性。一个可维护的系统可以快速恢复正常运行。
    硬件和固件协同支撑日志记录,帮助识别和隔离故障,让操作者可以进行预防性或主动性的维护。如果出现停机,可以快速地将系统重新上线,减少维护成本,并减轻停机对企业的后果。

  3. 维护数据完整性。
    RAS 技术提供了多种机制来防止数据损坏并纠正出错的数据。当检测到错误数据时,会确保它在可控制的范围内,避免引起更严重的问题。

x86 HPC ras 调研

错误分类

  • 可纠正错误 CE
    • 当检测到可纠正错误CE时,对错误位置进行标记,并通过对应模块的RAS技术快速修复错误,用户不会感知到这类错误的发生
  • 不可纠正错误 UCE
    • 尝试对故障进行隔离。比如通过隔离内存坏块、总线降频等手段,维持系统的运行。若发生了更为严重的故障,系统直接宕机,这时需要通过带外管理软件HDM恢复或重启系统
  • 硬件永久性故障
    • 需要更换新的硬件或者启用备用设备进行修复。通过对部分硬件的热插拔功能,可以支持用户在系统不断电的情况下,进行故障设备的更换,使服务器恢复正常工作。

故障上报

RAS 技术主要是通过 MCA 机制、AER 机制实现的 009

MCA

MCA (Machine Check Architecture) 机制可以上报并尽可能地修复系统总线、ECC、奇偶校验、缓存和 TLB 等等错误,识别故障源并将故障信息记录在 MC Bank 中。

MCA 是一个跟随着新处理器的发布不断增加新的特性和增强功能而不断进化的技术。

MCE (Machine Check Exception) 的产生通常是由于以下几个原因:

  1. 违反了主板设计指南,例如因为布线导致信号的干扰和完整性问题。
  2. 处理器工作在非正常的状态,比如超频等进而导致处理器出现意料之外的行为。
  3. 环境因素,比如环境太热,太冷,潮湿或者有辐射。
  4. 风扇或者散热器安装的有问题导致过热。
  5. 没有及时的升级 micorode,导致有些 fix 没有集成进来。
  6. BIOS 或者 OS 的配置有问题导致 MCE 的异常处理没有很好的工作。
  7. 板子上的设备如果外插卡,内存条等有问题也可能会导致 MCE。

在 MCA 架构的出现之前,OS 对 MCE 的处理非常有限,经常就是简单的重启系统
对于管理员而言,简单的系统重启难以接受,而且出错现场经常无法保存,从而无法排错。即使能够保留下一些日志,除非有很强的专业知识,否则完全不知道真正产生错误的原因是什么。

这些问题都在新的 MCA 中得到了解决和改进。利用新的 MCA 架构,CPU 可以按照配置产生 MCE(machine check exceptions)。
对于可以修正的(Correctable)MCE,硬件可以自动从错误状态中恢复,而且并不需要重启系统

早期的可修正的 MCE 并不需要产生中断,从 45nm Intel 64 处理器(CPUID 06H_1AH)开始引入了 corrected machine check error interrupt(CMCI)的机制,用户通过配置相应的 model-specific registers (MSR)允许可修正的 MCE 也会产生中断,软件可以捕捉到该中断并进行相应的处理。
对于不可修正的(uncorrectable)MCE,这时系统已经处于不再安全和可以信赖的操作模式,系统必须重启才能恢复。软件可以根据不同的错误源产生的错误类别,错误的严重程度,软件可以选择隔离错误,记录错误,甚至屏蔽错误源(对于不重要的非致命的错误类型)以避免干扰,或是必须要复位系统。
在新的 MCA 架构下,错误的记录管理,以及可读性都有了很大的提高。他可以

  • 帮助 CPU 设计人员和 CPU 调试人员诊断,隔离和了解处理器故障。
  • 帮助系统管理员检测在服务器长期运行期间遭受的短暂故障和与老化有关的故障。

MCA 恢复功能是基于 intel 至强可扩展系列处理器的服务器的容错功能的一部分。这些功能使系统在检测到未纠正的错误时可以继续运行。
如果没有这些功能,则系统将崩溃,并且可能需要更换硬件或重新引导系统。

通过 MCA 机制:

  • CPU 内部的可纠正错误和不可纠正错误均可上报并记录
  • 并纠正硬件可纠正错误。
  • 对于不可纠正错误,通常会进行热重启。
    MCA 的作用域包括处理器中的所有模块,Core、Uncore 和 IIO(通过 IOMCA)

AER

IIO AER (Integrated I/O Advanced Error Reporting) 机制 - PCI Express 的可选扩展功能
它提供了比标准 PCI Express 错误报告机制更强大的错误报告功能,包括 PCI Express AER、Traffic Switch、IRP、IIO 核心、英特尔 VT-D、CBDMA 和其他特定于英特尔的扩展。

负责侦测、记录并发送各种 IIO 模块下的子模块的错误信号,作用域包括 IIO 模块下的所有子模块,如 PCIe 接口,DMI,IIO 的核心逻辑和 Intel VT-d 等。

UPI

intel upi (Ultra Path Interconnect,极速通道互联)
UPI 可纠正错误上报:UPI 错误记录及信号发送的功能。

MCA 错误上报模式

处理器提供了以下几种不同的 MCA 错误上报模式: 010

  • Legacy IA-32 MCA 模式
    已经有几代英特尔处理器均支持 Legacy IA32 MCA 模式,该模式是大多数操作系统都支持的。
  • Corrupt Data Containmen 模式
    CDC(Corrupt Data Containment Mode)模式是对 MCA 机制的一种强化。当启动 CDC 模式并检测到不可纠正错误时,检测代理将设置“poison”位和数据一起转发给请求代理。
  • Enhanced MCA Gen1 (EMCA Gen1) Mode
    该模式是 Legacy IA-32 MCA 模式的第一代增强模式,是为了实现固件优先的错误报告模型。
  • Enhanced MCA Gen2 (EMCA Gen2) Mode
    第二代增强的 Legacy IA-32 MCA 模式。主要的目的是创建一个可通过操作系统启用的模式,并且进一步扩大固件第一模型(FFM)的错误报告范围。
  • IOMCA Mode
    允许 IIO 的不可纠正致命错误和不可纠正非致命错误通过 MCE 发送错误信号。

故障上报中断

011


错误处理流程

012

可纠正错误的处理 CE

如上图所示的橙色流程。

针对系统发生的可纠正错误,通过漏桶算法及设置可纠正错误阈值,可以实现在可纠正错误频繁发生时,触发 SMI (System Management Interrupt,系统管理中断) 中断通知 BIOS 进行错误处理,BIOS 接收到 SMI 中断请求后会根据不同的中断类型进行相对应的错误处理,在确保系统正常运行的同时,对发生错误的器件进行定位,隔离,搜集相关的错误状态寄存器信息,并上报 HDM 相关的错误事件及详细的错误状态寄存器信息,可供用户或服务器维护人员进一步分析问题发生原因。

不可纠正可恢复错误的处理 DE

如上图深绿色流程

对于不可纠正错误,如果这个错误是软件可恢复的(recoverable),则此错误并不会影响系统运行,只会将此错误数据将打上错误标记,并触发 SMI 中断,BIOS 收到此 SMI 中断后会搜集相关的错误寄存器信息,并对错误器件进行定位并上报 HDM 相关的错误信息及详细的错误状态寄存器信息。

不可纠正错误的处理 UE

如上图所示的黄褐色流程

如果 x86 系统发生了不可纠正且不可恢复的错误,CATERR_N 管脚会被拉低,这种错误会造成系统挂死,将会触发 HDM 的错误搜集程序,HDM 可以获取 x86 系统的错误状态寄存器信息,保证可以在系统挂死的情况下仍能在第一时间获取到错误现场信息,定位出错误根源并及时反馈给用户相关的信息。

错误日志记录

使用 MCA Bank、AER 状态寄存器、内存可纠正错误状态寄存器和 Intel UPI 错误状态寄存器实现 Core、Uncore 以及 IIO 模块的错误日志记录。

故障处理

内存故障处理


  • SDDC 提供错误检查和校正,用于校正 DIMM 上的单个 DRAM 颗粒故障(硬错误)和多比特故障。
  • ADDDC(MR),同样需要在 Virtual Lockstep 模式下启用,并且只支持可纠正区域。ADDDC 功能支持对于 x4 DDR4 的 DIMM,每个 IMC 纠正 2 个 DIMM 区域(Bank 或 Rank)

CPU 故障处理

当出现内核级错误,处理手段主要涉及到 Core Disable For Fault Resilient Boot 功能和 Core Corrupt Data Containment Enabled for DCU/IFU 功能。

  • Core Disable For FRB 功能 (Core Disable For Fault Resilient Boot)
    随着处理器内核数量的逐代增加,单个故障点从整个处理器转移到处理器内部的较小模块,比如单个 Core 或 LLC 的一部分。当出现了故障,除了可以禁用整个 CPU 之外,现在可以做到禁用特定的核。
    Core 的禁用需要保留至少一个 Core 是活动的,才能完成系统引导过程。
  • Core Corrupt Data Containment Enabled for DCU/IFU 功能
    处理器支持 DCU/IFU 的内核缺陷数据包容特性,在启用 MCA 恢复-执行路径的高级 RAS 特性的情况下,可以将某些类型的不可纠正数据错误上报为不可纠正可恢复错误(SRAR 类型的UCR)而非致命错误。
    “Error containment”位被一路传递给 DCU/IFU,从而允许隔离损坏的数据

PCIe 故障处理

  • PCIe Link Retraining and Recovery
    PCI Express 接口在出现链路降级时结合恢复机制,可以在不影响挂起的事务的情况下,进行重建链。如果在特定 lane 上出现了降级,恢复机制会按照 Platform Design Guide (PDG)定义的链路降级规则,降低链路宽度(例如,x16 链路将降级到 x8 链路)。如果在多个 lane 上出现降级,恢复算法会尝试在下一个允许的速度下重建链。
  • PCI Express Corrup Data Containment 功能(又称为 Data Poisoning)
    当接收端检测到不可纠正的数据错误时,使用“bad data”状态标识该错误数据,再将数据转发给目标,这种错误报告形式被称为“data poisoning”。接收 poison 数据的目标端,必须忽略数据,或者将数据带着“poison”标识存储起来。PCIE和一致性接口在事务分组中提供 poison 字段来标识错误数据。
    Data Poisoning 功能不仅限于发送的请求。需要用数据完成的请求也可以标识 poison 数据。

UPI 故障处理

  • Intel UPI Corrupt Data Containment (损坏数据抑制)
    • 当 UPI Date Poison 功能开启时
      Intel UPI 只是一个 poison 标识的管道。UPI TX/RX 接接收到 poison 数据,会继续将数据传送到目的地,并且不会触发错误信号或记录错误日志。这样将由数据的消费者来决定如何处理不可纠正的数据错误。
    • 当 UPI Date Poison 功能关闭时
      UPI 将看不到带有 poison 状态的数据,所有单元都返回到 Legacy MCA 模式,Intel UPI RX 收到 poison 数据,会发出一个错误信号并立即记录。
  • Intel UPI Dynamic Link Width Reduction
    在物理 lane 故障的情况下,支持从全带宽减小到 x8,半带宽支持仅用于 x8 位的最小集合,以允许任何单个数据通道失败。所得到的动态链路带宽减少模式是 lane[7:0]或[19:12],就是说只要不是所有故障都在[7:0]和[19:12]上,多 lane 故障就可以被恢复。

RAS 系统架构 (新华三)

  • HDM:故障定位系统的核心,它负责故障的收集、汇总和分析,并通过 Web 管理界面事件日志以及故障告警等方式向客户呈现。
  • 处理器平台:服务器采用 Intel Skylake 至强 CPU 平台,该平台较上一代基础上增强了 RAS 的能力,增强了对处理器、内存、PCIe 设备硬件故障的管理能力。
  • CPLD (Complex Programmable Logic Device):向下与各个硬件模块,包括电源、风扇以及其他底层硬件(除 CPU、内存、硬盘和 PCIe 标卡外)接口,捕获硬件异常状态,向上与 HDM 互连,传递故障信息。
  • BIOS:主要实现 CPU、内存、PCIe 以及存储设备的故障收集和定位,向 HDM 提供故障定位的结果,对 OS 层面来说,BIOS 提供 WHEA 等 OS 级故障管理的接口。
  • FIST(可选部件):FIST 服务器配套管理软件。SDS 日志会记录服务器平台在每个使用周期过程中产生的从硬件到软件,从主 CPU 到 BIOS、OS 到 BMC 的大小事件。SDS 日志需通过 FIST 来解析。根据该功能查找服务器的使用记录或判断服务器的健康状况,客服或者工程师可以追寻服务器健康问题的蛛丝马迹,快速定位问题,从而提高服务器的可服务性。
  • IFIST(可选部件):iFIST 是一款内嵌于服务器的单机管理工具,通过 iFIST 可以配置 RAID、安装操作系统、安装驱动程序和诊断服务器健康状况,以满足用户对单台服务器进行直接管理的需求。
  • 客户界面:主要通过 HDM 的 Web 界面,可以方便客户在远程或者本地进行系统维护工作,当然在主要部件上也会有故障指示灯。
  • 各类协议:故障管理系统中所用到的接口、协议包括:LPC,PECI,PCIe,UART,I2C,SMBUS,LocalBus 等。

RAS 功能表 (新华三)

RAS 功能一览表 016

RAS 功能简介

RAS 功能简介 019

riscv reri architecture

这个应该是类似于 X86 的 MCA

介绍

通过内存映射寄存器接口提供错误报告的标准机制,提供记录检测到的错误(包括其严重性、性质和位置)的功能,并配置了向 RAS 处理程序组件发送错误信号的方法。RAS 处理程序可以使用此信息来确定合适的恢复操作,这些操作可能包括:

  • 终止计算(例如,终止一个进程等)
  • 重新启动部分或全部系统等,以便从错误中恢复。

此外,该规范应支持软件发起的 RAS 处理程序的错误记录、报告和测试。
最后,该规范应提供实现错误处理的最大灵活性,并应与其他标准(如 PCle、CXL 等)定义的 RAS 框架共存。

错误级别

  • CE Corrected error. 同 x86 CE (可纠正错误)
  • UDE Uncorrected deferred error. 同 x86 DE (不可纠正可恢复错误)
  • UUE Uncorrected urgent error. 同 x84 UE (不可纠正错误)

feature

  • 标识错误严重等级和错误代码
  • 内存映射的错误记录寄存器, 错误记录 bank
  • 规则- 高优先级的错误记录可以覆盖低优先级的错误记录 (UUE > UDE > CE)
  • 可纠正错误 (CE) 计数
  • RAS handler 用于测试的错误注入机制

错误上报

主体:

  • riscv hart
  • 内存控制器

可支持一个或多个错误记录 bank
每个错误记录对应于组件的一个硬件单元,并报告由该硬件单元检测到的错误。
一个硬件单元可以实现多个错误记录。
由于组件中的一个或多个硬件单元检测到错误,或由于一个硬件单元检测到一个或多个错误,所以同一时间有一个或多个错误记录是有效的.

寄存器

每个错误记录对应一组寄存器,用于控制该错误记录,报告状态、地址和其他与该错误记录中的错误有关的信息

比较重要的 csr:

  • Error bank information (bank_info)

    系统的供应商将 inst_id 定义为组件的唯一标识符。返回值为 0 表示该字段未实现。
    N_err_recs 字段表示错误记录的数量。

  • Summary of valid error records (valid_summary)

    • sv=1 时, 软件通过 valid_bitmap 确定 bank 中的哪条错误记录是有效的, 哪条有效, 就说明哪组 i csr 是有效的. i 表示第 i 个错误记录的 index.
      一条错误记录有一组 csr , 由 status_i, control_i, info_i, addr_i 等组成.
    • sv=0 时, 软件需要遍历 status_i.v 来确定 error 记录是否有效.
  • Status register (status_i)
    The status_i is a read-write WARL register that reports errors detected by the hardware unit

    • v = 1 代表该条错误记录有效
    • ce 为 1, 代表该错误记录为可纠正错误
    • de 为 1, 代表该错误记录为不可纠正可恢复错误
    • ue 为 1, 代表该条错误记录为不可纠正验证错误
    • at 代表该条错误记录所涉及的地址是 VA/HPA/GPA 中的哪个
    • c 为 1, 表示错误可能是可控的,但 RAS 处理程序可能能够也可能无法从此类错误中恢复系统。RAS 处理程序必须根据错误记录中提供的附加信息(如检测到损坏的内存地址等)做出恢复决定。
    • pri 代表该条错误记录在该类错误中的优先级. 数越大优先级越高
    • mo 为 1, 代表相同类型的错误已发生过多次
    • tt 代表导致该错误记录时的访问类型, 有隐式/显式读/写等
    • iv=1 代表该条错误记录携带了额外信息, 额外信息被写入到 info_i 中.
    • siv=1 代表该条错误记录携带了额外补充信息, 额外补充信息被写入到 suppl_info_i 中.
    • tsv=1 代表该条错误记录携带了时间戳, 时间戳写入到 timestamp_i 中
    • scrub=1 代表该条错误记录为 CE, 且地址上的错误数据已经被纠正.
    • ec (error code), 代表该条错误记录的错误识别代码

    • cec (corrected-Error-counter), 当 control_i.cece=1 时, cec 代表当前已发生 CE 的错误记录的总计数.
  • Control register (control_i)

    Control_i 是一个读/写 WARL 寄存器,用于控制错误库中相应错误记录的错误报告。
    The ces, udes, and uues are WARL fields used to enable signaling of CE, UDE, and UUE respectively When they are logged.

    • control_i.sinv 清除 status_i 的 v 位.
    • else = 1, 记录错误记录的错误上报功能启用. =0, 关闭, 关闭时 CE 错误的硬件纠错机制仍然是开的, 建议在关闭时硬件组件继续对由硬件组件产生或存储的数据产生错误进行检测和纠正. 建议在关闭的情况下仍然抑制病毒数据
    • ces/udes/uues 分别控制 CE/UDE/UUE 错误的错误上报.

      错误记录产生的信号除了引起中断/事件通知外,还可以用来携带额外的信息,以帮助平台中的 RAS 处理程序.
      错误信号可以通过平台特定的方式配置中断类型, 通知平台中的 RAS 处理程序。例如,高优先级的 RAS 信号可被配置为引起高优先级的 RAS 本地中断、外部中断或 NMI,低优先级的 RAS 信号可被配置为引起低优先级的 RAS 本地中断或外部中断。
    • cece = 1, 打开 status_i.cec (CE 错误计数)
    • eid (error-injection-delay), 错误注入延时. 前面讲了错误注入机制是为了测试目的. eid 中有值时, 开始以特定的频率向下递减, 当 eid 达到 0 时, 产生一条错误记录, 错误记录就是对应 status_i 中的错误记录, 在错误上报开关打开后, 会产生对应的中断通知 RAS 处理程序.
      只会产生错误记录, 并不会产生错误反映到硬件上, 因此不是用来验证硬件错误的.
      软件应确保该机制不会被滥用而产生安全问题.

  • Address register (addr_i)
    The addr_i WARL register reports the address associated with the detected error

  • Information register (info_i)
    The info_i WARL register provides additional information about the error when status_i.iv is 1
    此字段结合 status_i.ec 错误代码看, 可用于报告特定于错误的信息,以帮助定位失败的组件、指导恢复操作、确定错误是暂时的还是永久的,等等。该字段可用于报告有关组件中错误位置的更详细信息,例如,检测错误的集合和方式、出错的奇偶校验组、ECC 错误状态、协议 FSM 状态、导致断言失败的输入等。

  • Supplemental information register (suppl_info_i)
    与 status_i.ec 结合, 代表特定的信息.

riscv ras 硬件方案 (develop simple)

https://riscv-europe.org/media/proceedings/posters/2023-06-06-Daniele-ROSSI-abstract.pdf

  • Error Mux
    多路错误接收复用器, 接收硬件错误信号, 根据硬件错误信号生成错误消息 (包括错误识别代码, 错误类型等), 将错误消息存放到 FIFO Buffer 中.

  • DE Controller 处理不可纠正可恢复错误。
    为了执行它的任务,读/写信号也是必需的。实际上,如果在读取操作期间访问了存储带有 DE 的数据的内存位置,那么这些有毒的数据将被消耗掉。因此,需要将 DE 升级为紧急错误 UE。相反,跟踪一个写操作到一个有延迟错误的位置,允许我们使相应的错误记录无效,因为在这种情况下,旧的数据将被新的数据覆盖。

  • Main controller
    用于选择用于定位错误记录的寄存器组, 采用了一种全组相连的 cache 缓存策略, 在错误产生时, 它会找到空闲的 bank (即确定 bank csr 组的 index), 将该错误记录存到该组寄存器下.
    如果所有错误记录都被占用了 (没有空闲的), 由它选择丢弃新的错误记录, 或者按照优先级将新的错误记录覆盖掉旧的. 优先级的顺序 UE>DE>CE, 对应其中每个子类别, 会按照 status_i.pri 来排序.

  • FIFO Buffer, 缓存来自 Error Mux 的错误记录消息

  • Error Record Banks
    即对应 Feature Register, 即前面介绍的 riscv reri architecture 中的所有的寄存器. 这些 csr 是 memory-mapped. 应该不在 cpu 内部, 而在 soc 范围, 设计时需要符合 riscv reri architecture 规范.

  • IRQ Gen 单元
    负责在不同场景中产生中断信号, 需要根据 riscv 寄存器组中 control_i 的设定来判断是否要生成中断信号. 不同的错误生成不同的中断 (根据错误级别可配置本地中断、外部中断或 NMI)

Linux

Documentation/admin-guide/ras. Rst
主要是 EDAC (Error Detection And Correction)
包括两个子系统,

  • Edac_mc
    负责收集内存控制器报告的错误
  • Edac_device。
    负责其他控制器(比如 L3 Cache 控制器)报告的错误。
    通常是把控制器的驱动写成一个 platform device,然后在 probe 的时候注册为 edac_mc 或者 edac_device。
1
2
3
4
edac_mc_handle_error()
edac_raw_mc_handle_error()
edac_device_handle_ce()
edac_device_handle_ue()

这些报告函数主要干这些事:

  1. Printk:把错误打印到 print ring buffer 里面
  2. Trace_mc_event:把错误写到 ftrace 的一个跟踪项中
  3. 统计:把这个错误报告到根据 DIMM 条,Rank,Row 的分类进行统计,为后续进行硬件替换提供参考
  4. 如果硬件报这是个 UE,而且这个控制器要求 UE 即停机,则复位系统

Printk 是个参考,是不适合正式处理的
Trace_xxx 是个跟踪,不开跟踪就不能工作,统计不能用于单个处理。

所以,当我们给 Linux 实现 RAS 特性的(硬件的)时候,必须有意识地用 page_fault 一类的异常来控制传播范围,只把中断的报告看作是统计上的补充。

当硬件侦测到一个错误,它有两种方法报告 CPU:

  • 一种方法是中断。这是个异步的过程,很容易造成传播范围不可控。

    • 如果这是 CE 的,错误已经被硬件主动修复,或者可以有手段修复(比如通过产生同步异常)。
    • 是 UE 的,这基本上就要导致停机乃至整体隔离了。
  • 第二种错误的报告方法是把错误随着 CPU 核的读写响应(消息)返回给 CPU,让 CPU 产生一个同步异常

    • 这种异常体现在 CPU 核上,就是一个 page_fault 中断,只是不同的类型,CPU 可以通过给对应的进程发 SIGBUS 一类的信号来控制这个错误。

Bios uefi

ACPI APEI 表

EDAC 是比较原始的实现,需要为每个平台的控制器写独立的驱动。ACPI 标准的 APEI 表从 BIOS 层提供了标准的报告形式。APEI 是 ACPI Platform Error Interface 的缩写,它包括多张表:

  1. BERT: Boot Error Record Table,这个用于记录前一次复位前 BIOS 记下来的错误,Linux 读到这个记录会打印出来,以便知道比如上次因为 UE 而服务的原因
  2. EINJ:Error INJection,BIOS 提供的硬件故障注入的接口,Linux 把这个封装成 debugfs 的属性了,可以通过这个注入需要的硬件错误
  3. ERST:Error Record Serialization Table,这个表用于配合 BERT,从 OS 侧辅助 BIOS 把临时前的错误信息保存到持久设备中。做硬件的同学要考虑适配这套框架,保证错误可以被固化到持久的存储中
  4. HEST: Hardware Error Source Table,这个描述系统有多少个错误检测设备,Linux 中这个被实现为多个(每条记录一个)平台设备
  5. GHES:General Hardware Error Source,这是硬件报错的接口。Linux 中,这个被实现为 HEST 定义的设备的驱动,每个那样的平台设备,Probe 到这个驱动上后,再注册为一个 edac_mc 设备,这样就和 EDAC 框架结合起来了

从这里可以看到,APEI 一定程度上是基于 EDAC 框架的,但它同时也提供独立于 EDAC 之外的功能。所以,高级的标准的服务器,更应该选择的是走 APEI 路线,而不是 EDAC 的路线。

只要硬件提供 APEI 接口,Linux 上就不需要额外的驱动了。

异常内存页隔离

如果我们发现异常的内存问题:

  • 一种办法当然可以立即停机。
  • 还有一种方法是隔离掉这片内存。
    这个功能依然做在 GHES 上,ghes_handle_memory_failure() 调用 mm/mm-failure.c 中的异常函数,Linux 会把这个 page 标记为 HWPOISON 的,之后相关的页,VM 或者进程就会被隔离掉。

不能避免错误被传播出去。但对一般数据中心来说,可能也够了。对大部分数据中心来说,你能说你这个节点不可信就够了,本来也不指望你还能提供内存双备这种不计成本的高级特性来。

Reference

AMD64 Architecture Programmer’s Manual, Volume 2: System Programming
Intel Xeon Processor E7 Family: Reliability, Availability, and Serviceability - White paper
Arm® Reliability, Availability, and Serviceability (RAS) Specification - Armv8 Architecture Profile, July 2019.
RERI (RAS Error-record Register Interface) task group. URL:https://lists.riscv.org/g/tech-ras-eri.

RISC-V Better Atomics (Load-Acquire/Store-Release)

是 RISC-V 体系结构的一种扩展,旨在提供更强大的原子操作支持。
在并发编程中,原子操作是一种确保多个线程或处理器可以安全地对共享变量进行操作的机制。原子操作需要满足一定的一致性和同步性质,以避免数据竞争和不确定的行为。

RISC-V Better Atomics 扩展引入了一对新的原子指令,即 Load-Acquire 和 Store-Release。

  • Load-Acquire 用于读取共享变量,并确保读取操作具有”acquire”语义,即保证在此原子操作之前的所有存储操作都可见。
  • Store-Release 用于写入共享变量,并确保写入操作具有”release”语义,即保证在此原子操作之后的所有加载操作都能看到最新的值。

使用 RISC-V Better Atomics 可以更好地控制并发访问共享变量的顺序和一致性。它提供了更细粒度的原子操作语义,以满足高性能和并发编程的需求。

在 RISCV-V 的 A 扩展下

  • Load-Acquire 指令通常以 lr.amoswap 的形式出现,例如 lr.w(Load-Reserved Word)用于加载一个字(32位数据)并获取内存序。
  • Store-Release 指令通常以 sc.amoswap 的形式出现,例如 sc.w(Store-Conditional Word)用于将一个字(32位数据)存储到内存中并释放内存序。

Saturating Operations

饱和操作(Saturating Operations)是一种特殊的操作,用于处理数值溢出的情况。当执行某种算术操作时,如果结果超出了数据类型的表示范围,饱和操作将会将结果截断为数据类型所能表示的最大或最小值,而不是简单地截断为溢出的结果。

RISC-V 架构中的饱和操作主要应用于整数数据类型。以下是一些常见的饱和操作指令:

  1. SLL.SAT:饱和左移指令,将一个寄存器中的整数左移指定的位数,并将结果截断为数据类型的最大或最小值。
  2. SRA.SAT:饱和右算术移位指令,将一个寄存器中的整数右算术移位指定的位数,并将结果截断为数据类型的最大或最小值。
  3. SRL.SAT:饱和右逻辑移位指令,将一个寄存器中的整数右逻辑移位指定的位数,并将结果截断为数据类型的最大或最小值。
  4. ADD.SAT:饱和加法指令,将两个寄存器中的整数相加,并将结果截断为数据类型的最大或最小值。
  5. SUB.SAT:饱和减法指令,将两个寄存器中的整数相减,并将结果截断为数据类型的最大或最小值。

这些饱和操作指令使得处理数值溢出的情况更加方便和可控,避免了溢出错误对程序执行的影响。

需要注意的是,饱和操作并不是所有 RISC-V 架构中都支持的标准指令。具体支持的指令和操作会根据特定的处理器和实现而有所不同。因此,在编写程序时,建议参考相关的处理器手册或编程指南,以了解特定处理器对饱和操作的支持情况和具体的指令格式。

Half-width floats (Zfh)

半精度浮点数(Half-width floats)是一种浮点数表示格式,用于表示较小的浮点数范围。在 RISC-V 中,半精度浮点数采用 IEEE 754 标准的半精度浮点格式。

半精度浮点数使用 16 位(2 字节)来表示一个浮点数,其中包括符号位、指数位和尾数位。具体的格式如下:

  • 符号位(1 位):用于表示浮点数的正负。
  • 指数位(5 位):用于表示浮点数的指数部分,可以表示范围为 -14 到 +15。
  • 尾数位(10 位):用于表示浮点数的尾数部分。

半精度浮点数相比于单精度浮点数(32 位)和双精度浮点数(64 位)具有较小的范围和精度,但占用更少的存储空间。在一些资源受限的场景中,如嵌入式系统或移动设备,半精度浮点数可以用于节省存储空间和提高计算效率。

玄铁 910 支持半精度浮点类指令

Bitmanip (wave 支持 zba and zbb)

RISC-V Bitmanip 扩展是 RISC-V 指令集架构的一部分,用于进行位操作和位操作相关的操作。Bitmanip 扩展引入了一组指令,以提供对位操作的支持,包括位移、位计数、位反转、位扩展等功能。

Bitmanip 扩展的主要目的是增强 RISC-V 的位操作能力,使开发者能够更高效地处理位级操作和位级数据操作。这对于一些应用场景,如加密算法、图形处理、数据压缩等,非常有用.

Bitmanip 扩展引入了一些常用的位操作指令,例如:

  • clz: 计算无符号整数的前导零位数。
  • ctz: 计算无符号整数的尾部零位数。
  • pcnt: 计算无符号整数的位计数,即二进制表示中的位为 1 的个数。
  • slo: 逻辑左移操作,将指定位数的位从右边移入。
  • sro: 逻辑右移操作,将指定位数的位从左边移入。
  • rol: 循环左移操作,将位向左循环移动。
  • ror: 循环右移操作,将位向右循环移动。

这些指令使得开发者能够更高效地进行位级操作和位级数据处理,简化代码编写和提高运行效率。

需要注意的是,具体的 Bitmanip 指令和功能会根据 RISC-V 架构扩展的版本和实现而有所不同。因此,建议查阅相关的处理器手册或编程指南以获取详细的信息和指令使用示例。

kingv 支持 Bitmanip extension

Zba扩展(Bitmanip Extension)引入了用于位级操作的指令,包括位逻辑操作(AND、OR、XOR)、位计数、位提取和位字段插入。这些指令可以高效地操作整数寄存器中的单个位和位字段,从而实现对位的精细控制。
Zbb扩展(Bitmanip Extension B)进一步增强了位操作的功能,引入了额外的指令用于位排列、位翻转和位计数等操作。这些指令提供了更高级的位操作功能,在密码学、数据压缩和信号处理等各种应用中非常有用。
Zba和Zbb扩展都是RISC-V体系结构的可选功能,在支持这些扩展的处理器中实现。要使用这些扩展,软件工具和编译器需要提供支持,以生成使用Zba和Zbb指令的代码。具体实现或处理器型号的可用性和对这些扩展的支持可能有所不同。

玄铁 C910 支持扩展的位操作指令

J extension (Zjpm+Zjid Shougun 支持 J extension?)

RISC-V J 的扩展旨在使 RISC-V 成为传统解释或 JIT 编译的语言或需要大型运行时库或语言级虚拟机的语言的一个有吸引力的目标, 包括 C# JAVA python 等.

https://github.com/riscv/riscv-j-extension

RISC-V 指针屏蔽(PM)是一个功能,当启用时,会导致 MMU 忽略有效地址的前 N 位。使得这些比特可以以应用程序选择的任何方式使用。所描述的扩展版本专门针对标签检查。当一个地址被访问时,存储在被屏蔽位中的标签与基于范围的标签进行比较。这被用于动态安全检查器,如 HWASAN[1]。这样的工具可以应用于所有的特权模式(U、S 和 M)。

Shougun 支持 J extension?

ABI gaps

需要 TLSDESC 等

TLSDESC

RISC-V TLSDESC,全称为 Thread Local Storage Descriptor,是 RISC-V 指令集架构的一部分,用于支持线程本地存储(Thread Local Storage,TLS)的访问。

TLS 是一种机制,允许每个线程在共享内存的基础上拥有自己独立的数据区域,这对于多线程编程非常重要。TLS 可以用于存储线程特定的数据,例如线程的局部变量或全局状态。

RISC-V TLSDESC 扩展引入了以下指令:

TLSDESC_CALL (TLS Descriptor Call):调用 TLS 描述符以获取线程本地存储数据的地址。
TLS 描述符是一个特殊的数据结构,用于获取线程本地存储数据的地址。TLSDESC_CALL 指令用于调用 TLS 描述符,并将返回的地址存储在指定的寄存器中,以便后续访问线程本地存储数据。

需要注意的是,RISC-V TLSDESC 扩展是可选的,并不是所有的 RISC-V 架构都支持该扩展。具体的支持情况可以参考特定处理器的文档或规格说明。

TLSDESC 扩展的引入使得在 RISC-V 架构上能够更方便地使用线程本地存储,提供了对多线程编程的支持

arm 中通过 TPIDR_EL0 寄存器,每个线程可以访问自己的 TLS 数据,而不需要使用全局变量或其他机制。线程局部存储是一种为每个线程分配独立内存空间的机制,用于保存线程特定的数据。
通过将 TLS 基址存储在 TPIDR_EL0 寄存器中,线程可以通过读取该寄存器来迅速获取自己的 TLS 数据的访问地址。

HWASAN

(Hardware-assisted Address Sanitizer)是一种基于硬件辅助的地址检测工具,用于检测和调试软件中的内存错误和安全问题。它类似于其他架构上的 AddressSanitizer(ASan)工具,但专门针对 RISC-V 架构进行了优化。

该实现依赖于 J-extension(又称 “指针屏蔽)的可用性。目前,这个扩展不是官方的
需要修改编译器 (llvm) 适配该实现

TEE

there’s an Ever ratcheting bar on the hardware features that are required whether it’s trusted execution environments confidential compute virtualization or even runtime detection of memory safety errors in the hardware

需要虚拟化实现的 TEE 或者硬件内存安全错误检测机制 (如 ARM 的 trustzone 或 sifive 的 worldguard 机制)

玄铁支持 VirtualZone 技术 (PMP+IOPMP)

virtualization

硬件虚拟化, 可以用来辅助实现内存的安全隔离机制
trusted execution environments confidential compute virtualization

C910 VS wave shougun/kingv

uefi 代码示例来自华为 hisi 1620

APEI

UEFI
ACPI Platform Error Interfaces

Provides a standard way to convey error info From Firmware to OS

BERT

Boot Error Record Table
Record fatal errors, then report it in the second boot
记录启动过程中的关键错误信息, 在下一次启动时报告错误.

  • 在 OS 未接管平台的控制权限之前 firmware(如 BIOS 或者 UEFI)检测到错误,导致系统无法继续启动,可以通过 BIOS/FIRMWARE 将这种类型的错误写入到特定的存储位置。这样一来,在下一次的正常启动过程中,OS 可以通过特定的方法将之前保存的错误读取出来分析并处理。
  • 系统运行过程中 firmware 检测到了致命错误,以至于 firmware 决定不通知 OS 而是直接重启(如 CPU 风扇突然坏了,瞬间过热,如果不立刻重启会烧毁 CPU),在重启前 firmware 可以记录下相关的错误信息以便之后分析出错原因

注:只有 BIOS/FIRMWARE 才有能力对 BERT 执行写入操作;对于 OS 而言,BERT 仅仅是一个只读的表。BERT 出现的意义在于希望采用一种统一的接口来记录特定类型的硬件错误(主要是一些致命的),从而简化 BIOS/FIRMWARE 和 OS 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ ApeiEntryPoint(ImageHandle, SystemTable)
\ -|+ if SetupData.EnRasSupport
"通过协议的 GUID 查找对应的协议, 查找acpi 表格协议"
\ -+ gBS->LocateProtocol(&gEfiAcpiTableProtocolGuid, NULL, &mAcpiTableProtocol);
"查找 ACPI 标准数据表协议, 操作系统和驱动程序可以获取系统硬件配置信息,进行系统初始化和配置,以及支持电源管理等功能。"
| -+ gBS->LocateProtocol(&gEfiAcpiSdtProtocolGuid, NULL, &mAcpiSdtProtocol);
| -+ gBS->AllocatePool (EfiReservedMemoryType, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), (VOID**)&mApeiTrustedfirmwareData) "分配内存池"
| - gBS->SetMem (mApeiTrustedfirmwareData, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), 0)); "memset 0"
| -+ OemInitBertTable (ImageHandle);
\ - BERT_CONTEXT Context;
| -+ BertHeaderCreator (&Context, BOOT_ERROR_REGION_SIZE); "Bert 表分配内存"
\ - Context->BertHeader = AllocateZeroPool (sizeof (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER));
| - Context->Block = AllocateReservedZeroPool (ErrorBlockSize);
"构造header 包含上图的 OEMID OEM_table_id creator_id等如 EFI_ACPI_ARM_OEM_REVISION EFI_ACPI_ARM_CREATOR_ID"
| - *Context->BertHeader = (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER) {
ARM_ACPI_HEADER(
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_SIGNATURE,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_REVISION
),
| - ErrorBlockInitial (Context.Block, EFI_ACPI_6_2_ERROR_SEVERITY_NONE);
| -+ BertSetAcpiTable (&Context); "初始化 Bert Table"
"ACPI 表格是存储着系统硬件配置信息和固件与操作系统通信信息的数据结构。在 UEFI 中,
有时需要向 UEFI 固件中添加自定义的 ACPI 表格,
以便在操作系统启动时提供特定的系统配置信息,或支持特定的硬件功能"
\ -+ mAcpiTableProtocol->InstallAcpiTable ( "通过Acpi 表格协议安装 Bert 表"
mAcpiTableProtocol,
Bert,
Bert->Header.Length,
&AcpiTableHandle);

linux driver

drivers/acpi/apei/bert.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-+ bert_init
\ -+ acpi_get_table(ACPI_SIG_BERT, 0, (struct acpi_table_header **)&bert_tab); "获取bert table"
| - region_len = bert_tab->region_length; "获取 bert_table length"
| - apei_resources_init(&bert_resources); "初始化 apei_resources 结构体"
"向bert_resources 下添加IOMEM 类型的资源,以便在发生错误时能够正确地处理这些资源。 "
| - apei_resources_add(&bert_resources, bert_tab->address, region_len, true);
| -+ apei_resources_request(&bert_resources, "APEI BERT");
| -+ boot_error_region = ioremap_cache(bert_tab->address, region_len);
| -|+ if boot_error_region
\ -+ bert_print_all(boot_error_region, region_len);
\ - "Error records from previous boot"
| -+ cper_estatus_print(KERN_INFO HW_ERR, estatus);
\ - " %s event severity: %s", severity "打印log错误级别"
| -+ foreach section
\ -+ cper_estatus_print_section(newpfx, gdata, sec_no)
\ - severity = gdata->error_severity;
| - printk("%s""Error %d, type: %s\n", pfx, sec_no, cper_severity_str(severity));
| - printk("%s""fru_id: %pUl\n", pfx, gdata->fru_id); "fru_id fru_text等, 见上图的表结构"
| - printk("%s""fru_text: %.20s\n", pfx, gdata->fru_text);
...

ERST

Error Record Serialization Table
Provides details necessary to communicate with on-board persistent storage for error recording

提供必要的详细信息, 协同存储错误记录

ERST 本质上是一个用来永久存储错误的抽象接口软件可以通过 ERST 表将各种错误信息保存到 ERST 中,再由 ERST 写入到可用于永久存储的物理介质中。ERST 并没有一个严格的定义来界定什么是“错误”,换言之,软件可以保存任何信息到 ERST 中,只要软件认为是有意义,有价值的信息就可以.
物理介质未必一定是 flash 或 NVRAM,可以是网络存储或者其他。

ERST 的主要作用就是用来存储各种硬件或者平台相关的错误,错误类型包括:

  • Corrected Error(CE)
  • Uncorrected Recoverable Error(UCR)
  • Uncorrected Non-Recoverable Error,或者说 Fatal Error。

换言之,只要是软件可以记录的错误,都可以将其保存到 ERST 当中。加上之前谈到的 BERT 表,这样一来,无论系统运行在哪个阶段,当出现硬件或平台相关的错误时,通过 APEI 接口,都有办法将错误保存下来。这样一来就可以在之后通过适当的方法将错误读取出来进行分析,从而加快定位产生错误的原因并加以解决。

EINJ

Error Injection Table
Provides a generic Interface which OSPM can inject hardware Errors to the platform without requiring platform Specific software.

提供通用接口方便 os 向硬件注入错误.
EINJ 的主要作用是用来注入错误并触发错误,或者说,EINJ 是一个用来测试的表。EINJ 可以注入各种类型的硬件错误,这些注入的错误不是模拟的,而是通过 EINJ 和底层 firmware 以及硬件配合真实产生的。通过 EINJ 注入的硬件错误是真正的错误,和硬件真实发生的错误没有差别。这样一来,平台设计者和软件开发人员可以使用 EINJ 在软硬件发布之前测试平台的软硬件环境是否可靠,是否具有足够的容错性以及完备性,而不必等到在平台发布之后的使用过程中出现错误时再来检测系统是否可靠。
EINJ 支持的错误注入方式非常丰富。从错误类型上划分,和 ERST 一样,包括 Corrected Error(CE),Uncorrected Recoverable Error(UCR),以及 Uncorrected Non-Recoverable Error,或者说 Fatal Error。从错误来源划分,可以分为 Processor,Memory,以及 PCI-E 设备等类型。通过交叉组合,至少有 9 种可以注入的错误。
至少有 9 种可以注入的错误。

  • 0x00000001 Processor Correctable
  • 0x00000002 Processor Uncorrectable non-fatal
  • 0x00000004 Processor Uncorrectable fatal
  • 0x00000008 Memory Correctable
  • 0x00000010 Memory Uncorrectable non-fatal
  • 0x00000020 Memory Uncorrectable fatal
  • 0x00000040 PCI Express Correctable
  • 0x00000080 PCI Express Uncorrectable fatal
  • 0x00000100 PCI Express Uncorrectable non-fatal
  • 0x00000200 Platform Correctable
  • 0x00000400 Platform Uncorrectable non-fatal
  • 0x00000800 Platform Uncorrectable fatal

使用 EINJ 进行错误注入有两个步骤:

  1. 根据需要产生错误注入需要的 trigger 表(trigger action table),这个 trigger 表是 BIOS/FIRMWARE 根据用户需要注入的错误类型动态生成的,不能人为手工构造;
  2. 是触发这个 trigger 表,让其在合适的位置产生需要的错误。至于产生了错误之后,如何处理错误,如何修复错误之类的事情,和 EINJ 无关。

EINJ 的注入过程基本上是一个 2 步操作:

  1. 使用 SET_ERROR_TYPE 这个 ACTION 向 EINJ 表中注入一个错误
  2. 根据第一步设定的错误,与 EINJ 相关的 firmware 会动态生成一个 Trigger Error Action 表,使用 GET_TRIGGER_ERROR_ACTION_TABLE 这个动作可以得到这个 trigger 表,然后操作这个 trigger 表可以触发之前注入的错误,从而达到测试特定错误类型的目的。

在整个过程当中,关键点是从 GET_TRIGGER_ERROR_ACTION_TABLE 动作完成后得到的 trigger 表的基地址。这个 trigger 表的基地址作为入口参数被 __einj_error_trigger 函数调用,最终完成错误触发。简单来说,__einj_error_trigger 需要完成两件事:

  • 根据 GET_TRIGGER_ERROR_ACTION_TABLE 返回的 trigger 表的地址(本质上是一个复合结构)进行相应的 IO 资源分配(这里的 IO 资源主要由 GAS 提供)
  • 调用 ACPI_EINJ_TRIGGER_ERROR 动作完成错误触发。

kernel driver

1
2
3
4
5
6
7
8
9
10
11
$ cd /sys/kernel/debug/apei/einj
# See which errors can be injected
$ cat available_error_type
# Set memory address for injection
$ echo 0x12345000 > param1
$ Mask 0xfffffffffffff000 - anywhere in this page
$ echo $((-1 << 12)) > param2
# Choose correctable memory error
$ echo 0x8 > error_type
# Inject now
$ echo 1 > error_inject

ACPI 5.0 BIOS 也可能允许注入特定于供应商的错误。在这种情况下,名为 vendor 的文件将包含标识信息,从 BIOS 中希望可以允许希望使用的应用程序使用特定于供应商的扩展,以告知他们正在 BIOS 上运行支持它。
所有供应商扩展都在其中设置了 0x80000000 位 error_type。文件 vendor_flags 控制对 param1 的解释和 param2(1 =处理器,2 =内存,4 = PCI)。

GHES of HEST

Generic Hardware Error Source - GHES
Hardware Error Source Table - HEST

HOW to get trigger: Notification Structure
WHERE are the error records:

Error Status Address
(GAS : Generic Address Structure)

HOW to release records’ mem:
Read Ack Register

在 HEST 中定义了很多硬件相关的错误源和错误类型。定义这些硬件错误源的目的在于标准化软硬件错误接口的实现。有了 HEST,当发生特定类型的硬件错误,如 PCI-E 设备产生了一个 Uncorrected Recoverable 类型的错误时,BIOS/FIRMWARE 有统一的方法更新特定的寄存器和内部状态,软件有统一的方法去处理和解析错误。HEST 中定义了很多硬件错误源,如 MCE、PCI-E、GHES 等等。

其中最为特殊也是最为重要的硬件错误源类型就是 GHES (Generic Hardware Error Source)。GHES 是一个通用硬件错误源,换言之,任何类型的硬件错误都可以使用 GHES 来定义,而无需使用之前提到的特定硬件错误源,如内存控制器错误等。

当前无论是软件还是 BIOS/FIRMWARE 的实现,基本上都是只使用 GHES 来实现 HEST 的功能,至于其他特定的硬件错误源,基本上都没有使用(PCI-E AER 的部分代码检测了 PCI-E 类型的硬件错误源)。

在 FFM (Firmware-First handling) 使能的情况下,一般而言,所有 CE 类型的错误通过 SCI 中断报告给 OS,然后 OS 在 HEST/GHES 中查表,检测并处理可能的硬件错误;所有 UC 和 Fatal 类型的错误通过 NMI 报告给 OS,然后 OS 在 NMI 的 handler 中查表,检测并处理可能的硬件错误。这些规定并不是硬性要求的,平台设计者完全可以根据需要使用 NMI 来处理所有的错误类型,包括 CE, UC 和 Fatal 类型的错误,也可以只使用 NMI 来处理 UC 和 Fatal 类型的错误,而使用轮询的方式来处理 CE 类型的错误。

linux kernel ghes 处理

Ghes_probe 函数中,根据 HEST 表中传递的检测错误类型,查看相关 kernel 配置选项是否支持。包括 arm 相关的 SEA 错误,NMI,本地中断。
调用 ghes_new 函数,初始化 struct ghes 结构。初始化 ghes 结构,映射表中 Error Status Address。为存放错误信息数据申请内存。

根据上报错误方式,注册不同的处理流程,包括如下:
(1)轮询方式,根据表中传递的 poll_interval 时间,创建定时器。在定时器处理函数 ghes_poll_func 中,调用 ghes_proc。在这个函数中:
A、读取 GHES 结构中传递的 Error Status Address。首先将读到的 struct acpi_hest_generic_status 结构拷贝到前面申请的内存中,检测相关错误信息长度是否合法。然后将后面的错误信息拷贝。
B、如上报错误的严重级别大于 GHES_SEV_PANIC,则将错误信息打印,清除错误状态以及记录错误信息的内存块。然后进入 kernel panic。
C、调用 ghes_do_proc 函数处理错误。这个函数中,获取错误数据块中各 section 中 section_type 以及 error_severity。同时判断 fru_id 和 fru_text 字段是否有效。 1>如为内存相关错误,以下为错误类型分类
* @ HW_EVENT_ERR_CORRECTED: Corrected Error - 表示检测到 ECC 纠正的错误
* @ HW_EVENT_ERR_UNCORRECTED : 表示 ECC 无法纠正的错误,但不是致命的错误 (可能是在未使用的内存区域,或者内存控制器可以从中恢复,例如,通过重新尝试操作)
* @ HW_EVENT_ERR_DEFERRED: Deferred Error - 表示处理不紧急的不可纠正的错误。这可能是由于硬件数据中毒,系统可以继续操作,直到中毒的数据被消耗。也可以采取主动的措施,例如,offlining 页面等。
* @ HW_EVENT_ERR_FATAL: 致命错误-无法恢复的不可更正错误。
* @ HW_EVENT_ERR_INFO: 规范定义了第四种类型的错误: 信息日志。
首先获取 struct cper_sec_mem_err 数据块。调用
Ghes_edac_report_mem_error 函数,这个函数中,将错误信息中包括错误类型,错误地址,内存颗粒,错误内存位置填充到错误报告 buffer 中(struct edac_raw_error_desc)。
错误类型如下:

把错误写到 ftrace 的一个跟踪项中,最后调用 edac_raw_mc_handle_error
分别通过 edac_ce_error 处理 ECC 类型的错误以及调用 edac_ue_error 处理其他错误。
最后调用 ghes_handle_memory_failure
如果
- 错误级别为可修复级别且 CPER_SEC_ERROR_THRESHOLD_EXCEEDED 置位(表示内核中止使用这个资源)
- 错误是可恢复的类型。满足上述条件之一则调用 memory_failure_queue 函数。这个函数在检测到页面的硬件内存损坏时由硬件错误处理程序调用。它调度错误页面的恢复,包括删除页面,杀死进程等。

如为 pcie aer 错误
PCIe AER 错误需要发送到 AER 驱动程序进行报告和恢复。GHES 的严重程度与以下 AER 严重程度相对应,并需要进行以下处理:
- GHES_SEV_CORRECTABLE -> AER_CORRECTABLE — 需要由 AER 驱动报告,但不需要恢复。
- GHES_SEV_RECOVERABLE -> AER_NONFATAL
- GHES_SEV_RECOVERABLE && CPER_SEC_RESET -> AER_FATAL
这两种情况都需要 AER 驱动报告和恢复。
GHES_SEV_PANIC 不会进行这种处理,因为内核必须将进入 panic 状态。

(2) 如果为外部中断,则通过 GHES 中传递的中断号,申请中断处理函数,在中断处理函数 ghes_irq_func 调用 ghes_proc。下面的处理流程和上面轮询一致。
(3) 同样的如果为 SCI 中断,调用 notifier_call 回调函数 ghes_notify_hed,遍历 ghes_hed 链表,分别执行 ghes_proc 函数,处理错误
(4) 如果为 NMI 中断,处理错误级别大于 GHES_SEV_PANIC,则直接 kernel panic。否则如配置 CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG,则调到工作队列处理函数 ghes_proc_in_irq 中。执行相当于下半部的处理过程。最终调用 ghes_do_proc 函数执行上述相关错误的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-+ ghes_probe(platform_device* ghes_dev)
\ - acpi_hest_generic *generic = ghes_dev->dev.platform_data;
| -+ switch (generic->notify.type)
\ -|+ case ACPI_HEST_NOTIFY_POLLED:
\ -+ ghes_add_timer(ghes);
| -|+ case ACPI_HEST_NOTIFY_EXTERNAL:
\ -+ acpi_gsi_to_irq(generic->notify.vector, &ghes->irq);
| - request_irq(ghes->irq, ghes_irq_func, IRQF_SHARED, "GHES IRQ", ghes); "申请中断. 中断处理函数为 ghes_irq_func"
| -|+ case ACPI_HEST_NOTIFY_SCI ACPI_HEST_NOTIFY_GSIV ACPI_HEST_NOTIFY_GPIO
\ - register_acpi_hed_notifier(&ghes_notifier_hed);
"`register_acpi_hed_notifier`是一个用于注册ACPI HED(Hardware Error Device)通知的函数。
HED是一种ACPI设备,用于监测硬件错误并报告给操作系统。HED通知允许内核注册一个回调函数,
当硬件错误发生时,内核会调用这个回调函数来处理错误。"
| - list_add_rcu(&ghes->list, &ghes_hed);
| -|+ case ACPI_HEST_NOTIFY_SEA:
"`ACPI_HEST_NOTIFY_SEA`表示SEA(Software Error Announcement)错误通知类型。
当系统中的硬件出现错误时,硬件错误设备(Hardware Error Device,HED)可以使用SEA通知类型向操作系统报告错误信息。
SEA通知类型用于通知操作系统发生了一个由硬件处理的错误事件,并提供相关的错误信息。"
\ -+ ghes_sea_add(ghes);
\ - list_add_rcu(&ghes->list, &ghes_sea);
| -|+ case ACPI_HEST_NOTIFY_NMI:
"`ACPI_HEST_NOTIFY_NMI`表示NMI(Non-Maskable Interrupt)错误通知类型。
当系统中的硬件出现严重的错误时,硬件错误设备(Hardware Error Device,HED)可以使用NMI通知类型向操作系统发出NMI中断,
以通知操作系统发生了一个严重的硬件错误事件。"
\ -+ ghes_nmi_add(ghes);
\ -+ register_nmi_handler(NMI_LOCAL, ghes_notify_nmi, 0, "ghes");
\ ---+ ghes_notify_nmi(cmd, regs)
\ -+ ghes_in_nmi_spool_from_list(&ghes_nmi, FIX_APEI_GHES_NMI)
\ -+ list_for_each_entry_rcu(ghes, rcu_list, list)
\ -+ ghes_in_nmi_queue_one_entry(ghes, fixmap_idx)
"将 GHES 错误信息封装成一个 NMI 消息,
并将该消息推入 NMI 队列。之后,当系统处理 NMI 中断时,
可以调用注册的 GHES 错误处理回调函数,从而处理 GHES 错误。"
\ -+ sev = ghes_severity(estatus->error_severity); "获取严重级别"
\ -|+ if sev >= GHES_SEV_PANIC "错误级别 > panic"
\ - __ghes_panic(ghes, estatus, buf_paddr, fixmap_idx); "调用panic"\
| - "构造 estatus_node, 从ghes 数据中获取 "
| -+ llist_add(&estatus_node->llnode, &ghes_estatus_llist);
"加入全局链表 ghes_estatus_llist 中"
"该链表在 ghes_proc_in_irq 中处理"
| -|+ case ACPI_HEST_NOTIFY_SOFTWARE_DELEGATED:
"`ACPI_HEST_NOTIFY_SOFTWARE_DELEGATED`表示软件委派错误通知类型。
当系统中的硬件出现错误时,硬件错误设备(Hardware Error Device,HED)可以使用软件委派通知类型向操作系统报告错误信息。
这种通知类型允许硬件错误设备将错误信息委托给操作系统的特定软件模块来处理,而不是直接由硬件设备进行错误处理。"
\ -+ apei_sdei_register_ghes(ghes);
\ -+ sdei_register_ghes(ghes, ghes_sdei_normal_callback, ghes_sdei_critical_callback);
"往下指向 arch 实现, 如 arm的 sdei_register_ghes 函数 drivers/firmware/arm_sdei.c"
\ ---+ ghes_sdei_normal_callback "回调"
\ -+ __ghes_sdei_callback(ghes, FIX_APEI_GHES_SDEI_NORMAL);
\ -+ irq_work_queue(&ghes_proc_irq_work);
"中断处理程序中调用`irq_work_queue`函数,将IRQ工作推入队列,
从而在合适的时机执行所需的工作。
即中断下半部的处理, 处理函数为 ghes_proc_in_irq"
\ ---+ init_irq_work(&ghes_proc_irq_work, ghes_proc_in_irq);

  1. Crypto Acceleration: Not only for speed, but code protection
  2. True Random-Number Generators
  3. Memory Encryption: No leak from external bus!
  4. Secure Boot
  5. Trusted Execution Environment: RISC-V PMP IOPMP / Virtual Zone
  6. Tamper Pins Detection: Detect unauthorized opening or tampering
  7. Bus Monitors: Advanced hardware security
  8. hsm Hardware Secure Modules

secure boot

Chain of Trust:

  • bootrom (ZSBL)
  • spl (FSBL)
  • opensbi
  • u-boot
  • kernel
  • rootfs
  • system
    bootrom spl opensbi u-boot kernel rootfs 之间形成链式校验

    spl opensbi u-boot kernel rootfs 需要进行加密并加签, 在启动过程中需要解密并验签

system 采用dm-verity 校验.
之间可以引入hsm 进行算法加密和校验请求

The main services and features of the secure boot code are:
• Provides a Root of Trust for the SoC
• Initiates a Chain of Trust for the SoC
• Performs the First-Stage Boot Loader (FSBL) digital signature validation
• Supports Device Identifier Composition Engine (DICE)
• Guarantees Post-Quantum Cryptography (PQC) crypto-agility
• Proposes a secure exchange protocol for code/data programming, tests, and key provi-sioning services
• Proposes a secure boot code secure patch mechanism
• Considers domestic cryptographic requirements
• Complies with good practices and security standards
• Provides high flexibility in the proposed options above (enable/disable)

Hardware Mechanisms and Requirements
Requirements:
• The secure boot code MUST be stored in the SoC internal ROM
• The secure boot code MUST boot the FSBL in less than boot-time ms
• The size for the ROM containing the secure boot code MUST be at least 64KB
• The secure boot code MUST be ready for PQC crypto-agility
• The public keys MUST be stored in the OTP
• The OTP size MUST be at least 2KB (for ECDSA, more if RSA) for the secure boot code
• The OTP size MUST be at least 4KB for crypto-agility
• The OTP controller MUST be able to grant/revoke read-and-write access privileges
◦ For all or parts of the OTP
◦ The DICE UDS read access MUST be deactivable by the secure boot code
◦ The DICE UDS write access MUST be disabled
• The OTP read/write access privileges MUST be based on time condition
◦ For all or parts of the OTP
• The OTP read/write access privileges MAY be based on authentication conditions
◦ For all or parts of the OTP
• The secure boot code MUST support the ECDSA p384/SHA384 digital signature algo-
rithm
• The secure boot code MUST support the SM2/SM3 digital signature algorithm
• The secure boot code MUST support the FALCON digital signature algorithm
• The secure boot code MAY support the ECDSA p256/SHA256 digital signature algorithm
• The secure hash SHA-384 implementation MUST be a hardware block (for performance
requirements)
◦ This hardware block MUST be accessible from the core executing the secure boot
code
• The secure hash SM3 implementation MUST be a hardware block (for performance
requirements)
◦ This hardware block MUST be accessible from the core executing the secure boot
code
• The ECDSA and SM2 algorithms MUST have hardware support for performance (e.g.,
modular multiplication)
◦ This hardware block MUST be accessible from the core executing the secure boot
code
• A communication interface MUST be available to the secure boot code, usually a UART
◦ This interface is used for initial and recovery programming
• A GPIO MUST be available for triggering the secure communication session
• The internal RAM size MUST be at least 64KB
◦ It depends on the size of the applets (a feature used in the communication protocol to
extend commands; it’s very helpful to simplify the secure boot code)
• The NVM where the FSBL is located MUST be available to the secure boot code in read
(for execution) and write (for programming) modes
• The debug availability policy MUST be specified
◦ If available, the secure boot code MAY be in charge of enabling it
• The secure boot code MUST contain anti-fault mechanisms during sensitive computations
◦ Note: The other subsequent parts of the secure boot process, too
• The secure boot code software validation MUST assess the robustness against faults
(and not be only a functional validation)
• The [rng] (for secure provisioning service) MUST be accessible by the secure boot code
• A [hash-function] hardware block and a managing FSM MUST be available for secure
boot code verification before start
• The secure boot code MUST be in charge of the DICE CDI computation for the FSBL
using the UDS from the OTP
◦ The UDS read access MUST be disabled after CDI computation
◦ The debug interface MUST NOT be available before UDS read access disabling
• The secure boot code MUST be written using good practices for secure coding, including
those presented in: https://www.ssi.gouv.fr/uploads/2020/05/anssi-guide-regles_de_pro-
grammation_pour_le_developpement_securise_de_logiciels_en_langage_c-v1.1.pdf
• The secure boot code MUST clean any sensitive information contained in the volatile
memory after its use
• The secure boot code MUST support GPT parsing for FSBL image booting
• The secure boot code MUST implement an anti-rollback protection mechanism
◦ The anti-rollback mechanism MUST support at least 32 versions
▪ It requires 128 bytes for version storage
◦ The anti-rollback mechanism MUST use the internal OTP

DCIE
DICE是一个用于设备身份、证明和数据加密的行业标准。它要求使用一些加密原语(哈希、HMAC)和每个SoC唯一的256位秘密,命名为UDS。这个UDS必须得到严格的保护,以防止嵌入式软件的任何读取访问。其原理是将一个独特的、可重复的秘钥与设备上运行的每个软件阶段层联系起来。最初的秘钥,也就是与FSBL绑定的秘钥,又称第0层,是由安全启动码使用FSBL的哈希值计算出来的。FSBL可以通过计算下一个CDI,对应于下一个软件阶段(如U-boot)来重现相同的方案,以此类推,直到达到最后一个应用阶段。然后,任何一层都有自己的秘钥(公钥对也可以从CDI中得到)。

Key Management

Public Keys:

  • SiFive Update Key (SUK)
    • This key is owned by SiFive, hardcoded in ROM, and used for the CUK initial pro-gramming using the digital-signature algorithm
  • SiFive Signature Key (SSK)
    • This key is owned by SiFive, and used for digital signatures during SXP sessions and the FSBL signature in phase #1, as long as no CUK/CSK has been programmed
    • The SSK value is hardcoded in ROM, and is used by default if no other key is pro-grammed in the OTP
  • Customer Update Key (CUK)
    • This key is owned by the customer, stored in OTP, and used for the CSK initial pro-gramming, revocation, and update using the digital-signature algorithm
  • Customer Signing Key (CSK)
    • This key is owned by the customer, stored in OTP, and, with its signature by the CUK, is used for the SXP sessions signatures, the FSBL signature using the digital-signature algorithm, and some secure debug data signatures
  • Debug Protection Key (DPK)
    • This key is owned by the customer, stored in OTP, and used for the secure debug authentication protocol, using the digital-signature algorithm
  • PQC Customer Signing Key (pqc-CSK)
    • This key is owned by the customer, stored in NVM, signed with the pqc-CUK, and, with its hash stored in OTP, is used for the SXP sessions signatures and the FSBL signature using the pqc-digital-signature algorithm, addressing crypto-agility
  • PQC Customer Update Key (pqc-CUK)
    • This key is owned by the customer, stored in NVM, and, with its hash stored in OTP, is used for the pqc-CSK initial programming, revocation, and update using the pqc-digital-signature algorithm, addressing crypto-agility
  • PQC Debug Protection Key (pqc-DPK)
    • This key is owned by the customer, stored in NVM, signed with the pqc-CUK, and, with its hash stored in OTP, is used for the secure debug authentication protocol using the pqc-digital-signature algorithm, addressing crypto-agility

Private Key Pairs:

  • ECC Key Pair (ECCK)
    • This [asymmetric-encryption] public key pair is owned by the customer, stored in OTP, unique per SoC, and in the read-controlled area for FSBL image decryption key encapsulation

Symmetric Keys:

  • Firmware Encryption Key (FEK)
    • This [encryption-algorithm] secret key is owned by the customer, unique per SoC, stored in OTP, and in the read-controlled area for SXP sessions and FSBL image decryption
  • Unique Device Secret (UDS)
    • The UDS is a DICE initial secret value, owned by the customer, unique per SoC, stored in OTP, and in the read-controlled area, for DICE FSBL (layer0) CDI computation

概述

virtio-scsi 提供了直接连接 SCSI LUN 的能力,并且也提供了继承目标设备特性的能力:
通过 virtio-scsi 控制器连接的虚拟硬盘或 CD,可以从 host 主机通过 QEMU scsi-block 设备实现物理 SCSI 设备的直通 (pass-through),这样就可以实现每个 guest 使用上百个设备,也提供了极高的存储性能。

guest 与 host 链路:
guest: app -> 文件系统-> Block Layer -> SCSI Layer -> scsi_mod
host: SCSI Layer -> Block Device Driver -> Hardware

前端 后端
virtio-scsi—qemu virtio-scsi
virtio-scsi—kernel vhost-scsi

直通方式

kernel vhost-scsi

QEMU负责对该PCI设备的模拟,只是把来自virtqueue的数据处理逻辑拿到内核空间了。QEMU需要告知内核vhost-scsi模块关于virtqueue的内存信息及Guest的内存映射,这样其实省去了Guest到QEMU用户态空间,再到宿主机内核空间多次数据复制; 但是,将queue的ID写到PCI配置空间的这步操作还是存在。

image-20240416111040666

通过vfio-pci直接进行SATA控制器直通

所有PC上的SATA控制器都在PCI总线上运行。您可以插入备用SATA控制器并通过它。所有连接的磁盘将直接传递给客户机。该解决方案几乎没有延迟或开销,并提供了最高的吞吐量。该解决方案的缺点是您将需要专门用于VM的第二个SATA控制器。来宾运行时,主机上的磁盘也将不可用。

image-20240416111048252

通过vfio-pci直接NVMe驱动器直通

与SATA控制器直通相似,通过NVMe驱动器也有助于提高性能。实际上,由于NVMe驱动器的疯狂吞吐量,性能提升甚至更高。开销也很低。
后端qemu直接对接宿主机上的块设备,不经过文件系统。但是这样的性能和中间有文件系统来对比,差别不会很大。

virtio-scsi

virtio-scsi功能是一个新的准虚拟化的SCSI控制器设备。它是KVM Virtualization的另一种存储实现的基础,它取代了virtio-blk,并改进了它的能力。它提供了与virtio-blk相同的性能,并增加了以下直接的好处。

  • 提高可扩展性–虚拟机可以连接到更多的存储设备(virtio-scsi可以处理每个虚拟SCSI适配器的多个块设备)。
  • 标准命令集virtio-scsi使用标准SCSI命令集,简化了新特征的添加。
  • 命名为virtio-scsi磁盘的标准设备使用与裸机系统相同的路径。这简化了物理到虚拟和虚拟到虚拟的迁移。
  • SCSI设备直通式virtio-scsi可以直接向guest 提供物理存储设备。
    Virtio-SCSI提供了直接连接到SCSI LUN的能力,与virtio-blk相比,显著提高了可扩展性。virtio-SCSI的优点是它能够处理数百个设备,而virtio-blk只能处理大约30个设备并耗尽PCI插槽。旨在取代virtio-blk,virtio-scsi保留了virtio-blk的性能优势,同时提高了存储的可扩展性,允许通过一个控制器访问多个存储设备,并使客户操作系统的重用成为可能。

pass-through the SCSI messages from the virtual machine kernel directly to the real device (virtio-scsi back end)
The virtio-scsi back end allows the guest to directly send SCSI requests back to the real device.
image-20240416111054089
All SCSI commands responses are sent by the real device, passing through QEMU. This mechanism allows the guest device to use all the features that the real device implements. Read and write requests from the guest are also sent directly to the real device.

1
2
3
-device virtio-scsi-pci,id=scsi0,bus=pci.0,addr=0x2 \
-device scsi-block,bus=scsi0.0,channel=0,scsi-id=0,lun=2,drive=drive-scsi0-0-0-2,id=scsi0-0-0-2 \
-drive file=/dev/disk/by-id/scsi-3600605b000a2c110ff0004053d84a61b,format=raw,if=none,id=drive-scsi0-0-0-2,cache=none

当fd有信号之后会唤醒eventfd等待队列上的对象,这里会执行vhost_poll_wakeup函数,该函数把work挂到vhost_dev的work_list中,然后唤醒vhost_dev的work线程,也就是在绑定用户态进程时创建的线程,vhost_scsi_handle_kick

1
2
3
4
5
6
vs->vqs[VHOST_SCSI_VQ_CTL].vq.handle_kick = vhost_scsi_ctl_handle_kick;
vs->vqs[VHOST_SCSI_VQ_EVT].vq.handle_kick = vhost_scsi_evt_handle_kick;
for (i = VHOST_SCSI_VQ_IO; i < VHOST_SCSI_MAX_VQ; i++) {
vqs[i] = &vs->vqs[i].vq;
vs->vqs[i].vq.handle_kick = vhost_scsi_handle_kick;
}

qemu 通过 vhost_dev_enable_notifiers -> virtio_bus_set_host_notifier 对几个vq绑定了ioeventfd,

guest os觉得有必要通知host对virtqueue上的请求进行处理,就会执行vp_notify(),相当于执行一次port I/O(或者mmio),虚拟机则会退出guest mode, 由kvm 处理guest store execption, kvm先判断自己能不能处理, 即查对应的mmio 区间是否注册了对应的处理函数, 这个地方由于ioeventfd的注册过程中注册了对应mmio的处理函数为 ioeventfd_write, 该函数给对应的fd发信号, 而vq上的poll返回后调用响应的handle_kick, 此处vhost-scsi 内核模块最终响应 vhost_scsi_handle_kick 函数处理vq, vq中封装的是scsi message. host 将scsi message 下发给hardware.

qemu virtio-scsi方案的演进,块设备模拟仍然是由qemu来做,只是把virtio backend放到了host kernel中,由kernel去处理virtqueue。 host kernel要处理virtqueue需要知道地址,因此qemu会把virtqueue的内存信息和guest的GPA-HVA的映射告知内核vhost-scsi模块,host kernel直接接收virtqueue中的请求并下发到后端,缩短了io路径,省去了host上用户态到内核态的拷贝

存储栈:
guest os: 文件系统层 -> 块设备层(block layer) -> scsi层(virtio-scsi 后端)
host os: scsi层 (vhost-scsi) -> hardware驱动 -> hardware

这里的直通是 通过 virtio-scsi – vhost-scsi 模拟了传统存储栈中的scsi layer, 替代了 scsi upper midi layer的功能, 创建出了 lun, lun是接收scsi cmd的实体, 每个lun需要有一个实体的hardware的驱动, 如对应sata设备会加载sd公版驱动.
上述scsi cmd最终转发到sd驱动中.

总结

非PCI方式的完整块设备映射,还是借助了virtio半虚拟化;
走PCI方式的直通, 通过vfio-pci实现的直接sata控制器直通和直接NVMe驱动器直通,这两种是真正意义上的passthrough,宿主机需要开启IOMMU;另外,需要对磁盘做vfio相关的配置,才能最终提供给虚机使用。

综上,

从virtio半虚拟化出发,为提高本地盘性能,可以将后端qemu virtio-scsi,替换为kernel vhost-scsi. 这种方式并未真正的直通, guest os的设备中断和数据请求都需要经过kvm, 这里所谓的直通只是将数据封装形式从之前的文件系统的消息转变为了scsi message, 该message 需要借助virtio 的vq在前后端(virtio-scsi – vhost-scsi)之间传递, 由这两个脚手架模拟了传统存储栈中的scsi layer.
存储栈参考 https://blog.csdn.net/Wang20122013/article/details/122090135

从直通方式提升性能,可以使用通过vfio-pci实现的直接sata控制器直通和直接NVMe驱动器直通这两种。

多分区镜像如何挂载

利用mount -o offset选项进行挂载。即偏移地址。

1
sudo mount -v -o offset=1048576 -t vfat sdimage.img ~/mount/boot/

Units: sectors of 1 * 512 = 512 bytes
一个扇区为512bytes。

1
2
3
4
5
sudo fdisk -l starfive-jh7110-VF2_515_v2.5.0-69-minimal-desktop.img
Device Start End Sectors Size Type
starfive-jh7110-VF2_515_v2.5.0-69-minimal-desktop.img1 2048 34815 32768 16M Linux filesystem
starfive-jh7110-VF2_515_v2.5.0-69-minimal-desktop.img2 34816 239615 204800 100M EFI System
starfive-jh7110-VF2_515_v2.5.0-69-minimal-desktop.img3 239616 3479518 3239903 1.6G Linux filesystem

如挂载第三个分区
offset = 239616 * 512

1
sudo mount -o offset=122683392 starfive-jh7110-VF2_515_v2.5.0-69-minimal-desktop.img test_mnt_dir 

qcow2 格式

制作

1
qemu-img create -f qcow2 image.qcow2 1G

挂载

1
sudo qemu-nbd --connect=/dev/nbd0 image.qcow2

connect 后, 如果该镜像中有分区的话, 需要等几秒中, 等分区节点出来

connect 时注意该镜像文件未被使用, 否则后续步骤会有问题

创建分区

1
2
sudo fdisk /dev/ndb0
n p 选start sector 回车 选end sector 回车 w 保存

创建文件系统

1
sudo mkfs.ext4 /dev/nbd0p1
1
2
3
4
5
6
7
8
9
10
sudo fdisk -l /dev/nbd0
Disk /dev/nbd0: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 664E2EB5-C2C5-414B-AFBC-A422A6627DC8

Device Start End Sectors Size Type
/dev/nbd0p1 128 20971392 20971265 10G Linux filesystem
1
2
sudo fdisk -l /dev/nbd0
sudo mount /dev/nbd0p1 mnt_dir

系统只有一个loop设备时, 需要将其他的loop设备卸载后, 才能挂载该节点

卸载

修改里面的文件修改完后, 要及时卸载

1
2
sudo umount mnt_dir
sudo qemu-nbd --disconnect /dev/nbd0

u-boot extlinux

conf

1
2
3
4
5
6
label l0
menu label Debian GNU/Linux bookworm/sid 6.0.0-6-riscv64 # menu 显示项
linux /boot/vmlinux-6.0.0-6-riscv64 # 内核
initrd /boot/initrd.img-6.0.0-6-riscv64 # ramdisk
fdtdir /boot/dtbs/ # dtb
append root=LABEL=rootfs rw noquiet root=LABEL=rootfs # rootfs

注意下面几项:

initrd

1
2
file initrd.img-6.0.0-6-riscv64
initrd.img-6.0.0-6-riscv64: gzip compressed data, was "mkinitramfs-MAIN_aDjhxR", last modified: Sun Jan 8 06:50:27 2023, from Unix

Linux kernel提出了一个RAM disk的解决方案,把一些启动所必须的用户程序和驱动模块放在RAM disk中,这个RAM disk看上去和普通的disk一样,有文件系统,有cache,内核启动时,首先把RAM disk挂载起来,等到init程序和一些必要模块运行起来之后,再切到真正的文件系统之中。

查看initramfs的内容

1
2
mkdir initrdtmp && cd initrdtmp
zcat ../initrd.img-6.0.0-6-riscv64 ../initrd.img-6.0.0-6-riscv64.gz|cpio -i --make-directories

initrd 不一定非得用 mkinitramfs 来制作, 使用制作busybox 时的 cpio -H newc 也是可以的

1
find .| cpio -o -H newc | gzip > ../busybox_rootfs.cpio.gz

linux

支持 bzImage Image 格式编译出来的vmlinux elf

fdtdir

fdtdir 指定 dtb的目录, 一般名字会根据Machine 参数指定
需要注意dtb 一定是匹配的.
qemu 使用时, 可以使用下面的命令生成对应的 dtb 文件, 注意cpu 内存 machine的配置要和 qemu 启动虚拟机的命令指定的参数一致

1
sudo qemu-system-riscv64 -machine virt,dumpdtb=qemu-riscv.dtb -cpu rv64 -m 2G -smp 2

append

指定kernel cmdline
如 rootfs 启动, rootfs 在 /dev/mmcblk1p3 节点

1
append root=/dev/mmcblk1p3 rw console=tty0 console=ttyS0,115200 init=/init

如qemu 启动, 在指定hda 等参数后, 生成的节点一般是vda
则使用下面的配置即可

1
append root=/dev/vda rw init=/init #或rdinit=/linuxrc

本根分区启动, 适用于只有一个分区的情况:

1
append root=LABEL=rootfs rw noquiet

u-boot 编译

注意一定是 S-mode的, 别选错了

1
2
make qemu-riscv64_smode_defconfig O=virt_build
make O=virt_build -j12

USB控制器类型

简单地讲,OHCI、UHCI都是USB1.1的接口标准,而EHCI是对应USB2.0的接口标准最新的xHCI是USB3.0的接口标准

  • OHCI(Open Host Controller Interface)是支持USB1.1的标准,但它不仅仅是针对USB,还支持其他的一些接口,比如它还支持Apple的火线(Firewire,IEEE 1394)接口。与UHCI相比,OHCI的硬件复杂,硬件做的事情更多,所以实现对应的软件驱动的任务,就相对较简单。主要用于非x86的USB,如扩展卡、嵌入式开发板的USB主控。
  • UHCI(Universal Host Controller Interface),是Intel主导的对USB1.0、1.1的接口标准,与OHCI不兼容。UHCI的软件驱动的任务重,需要做得比较复杂,但可以使用较便宜、较简单的硬件的USB控制器。Intel和VIA使用UHCI,而其余的硬件提供商使用OHCI。
  • EHCI(Enhanced Host Controller Interface),是Intel主导的USB2.0的接口标准。EHCI仅提供USB2.0的高速功能,而依靠UHCI或OHCI来提供对全速(full-speed)或低速(low-speed)设备的支持。
  • xHCI(eXtensible Host Controller Interface),是最新最火的USB3.0的接口标准,它在速度、节能、虚拟化等方面都比前面3中有了较大的提高。xHCI支持所有种类速度的USB设备(USB 3.0 SuperSpeed, USB 2.0 Low-, Full-, and High-speed, USB 1.1 Low- and Full-speed)。xHCI的目的是为了替换前面3中(UHCI/OHCI/EHCI)。

usb 层级

用lsusb -t还可以看到USB设备的层级关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@localhost xqk]# lsusb -t
/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=ehci-pci/2p, 480M
|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/8p, 480M
|__ Port 1: Dev 3, If 0, Class=Mass Storage, Driver=usb-storage, 480M


Bus 02.Port 1
表示第二个USB主控制器,Port号为1

Dev 1, Class=root_hub
分配的设备号为1,类型是root_hub

Driver=ehci-pci/2p
root_hub的类型是ehci(usb 2.0),总共有两个port

|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/8p, 480M
root_hub的其中一个port有个Hub设备,port的id是1,此Hub有8个port。

|__ Port 1: Dev 3, If 0, Class=Mass Storage, Driver=usb-storage, 480M
Hub的其中一个port有大容量USB设备,port的id为1.3(树状结构,依次以.作为低一级设备成员)

USB设备直通

虚拟机使用USB设备可以通过两种方式:

  • PCI 设备直通

    PCI usb card, 直接将 usb controller 直通给虚拟机

  • 设备直通

这里重点说下非PCI方式的usb 设备直通
qemu支持hostaddr,hostport以及vendorid,productid等信息的组合, 相对于libvirt,多了hostport的支持,hostport即是主机usb的port。

1
2
3
4
5
6
7
8
9
10
(1) vendorid+productid -- match for a specific device, pass it to
the guest when it shows up somewhere in the host.

(2) hostbus+hostport -- match for a specific physical port in the
host, any device which is plugged in there gets passed to the
guest.

(3) hostbus+hostaddr -- most useful for ad-hoc pass through as the
hostaddr isn't stable, the next time you plug in the device it
gets a new one ...

注意点

  • devnum会变化
    在host上同一个USB口上插拔U盘,会导致devnum的变化(增加),而port对应于USB的物理口,不会随着插拔U盘而变化,但是libvirt不支持传入port,只能自己适配开发。

  • 是否需要开启vt-d iommu
    通常情况下,直通物理设备(PCI)时需要开启vt-d,USB设备有些不同,USB设备是由controller控制,而各种类型的控制器都是qemu模拟的,只不过最终直接打开物理机上的USB设备而已,数据还是由qemu控制。因此USB设备直通不需要开启vt-d iommu。

  • No free USB ports
    模拟的控制器对应的port数量有限,如果要直通多个USB设备,就会造成port数目不够。piix3-uhci有两个port,ehci与nec-xhci有6个port(一般够用)。当数目不够时,可以添加hub设备来增加额外的port。

    1
    <hub type='usb'/>

    hub设备包含8个port。

  • 不要直通主机hub设备
    主机上的root_hub以及hub直通给虚拟机没有任何意义,一方面虚拟机中的root_hub是模拟控制器自带的,而hub是需要单独添加hub设备,另一方面直通hub设备,虚拟机占用主机hub,会导致主机上hub下的USB设备不可用。

qemu usb 非PCI 直通解读

qemu默认不支持 usb-host, 需要在编译时打开 --enable-libusb 编译, 依赖libusb 库.

按上一章的参数介绍, 简单看下代码结构
注意

USB设备是由controller控制,而各种类型的控制器都是qemu模拟的,只不过最终直接打开物理机上的USB设备而已,数据还是由qemu控制

qemu 命令行方式添加usb 直通设备

1
2
3
-device usb-host,hostbus=BUS,hostaddr=ADDR,id=[hostdev0]
-device usb-host,vendorid=VID,productid=PRID,id=[hostdev0]
# 对应lsusb的Bus xxx, Device xxx

注意上述命令需要以 sudo 运行 qemu 命令, 否则libusb 库无法打开给usb设备.

添加这个设备后, 最终会走到 TYPE_USB_HOST_DEVICE 设备的具现化函数中
TYPE_DEVICE -> TYPE_USB_DEVICE -> TYPE_USB_HOST_DEVICE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ usb_host_realize(USBDevice *udev, Error **errp)
\ - ldev = usb_host_find_ref(s->match.bus_num, s->match.addr);
"libusb 库函数查找 hostbus, hostaddr 匹配的usb设备"
| -+ usb_host_open(s, ldev, 0);
\ - bus_num = libusb_get_bus_number(dev); addr = libusb_get_device_address(dev);
s->dev = dev;
s->bus_num = bus_num;
s->addr = addr;
\ -+ usb_host_detach_kernel(s);
\ - libusb_detach_kernel_driver(s->dh, i); "从主机中解绑"
| - USBDevice *udev = USB_DEVICE(s);
| - usb_ep_init(udev);
| -+ usb_host_ep_update(s); "将设备绑定到 qemu 模拟的usb controller 上"
\ - libusb_get_active_config_descriptor(s->dev, &conf);
| -+ for i in conf->bNumInterfaces "遍历接口描述符"
\ - intf = &conf->interface[i].altsetting[0];
\ -+ for e in intf->bNumEndpoints "遍历端点描述符"
\ - endp = &intf->endpoint[e];
| - devep = endp->bEndpointAddress;
| - pid = (devep & USB_DIR_IN) ? USB_TOKEN_IN : USB_TOKEN_OUT; "是in 端点还是 out端点"
| - ep = devep & 0xf;
| -
| -+ usb_device_attach(udev, &local_err);
\ - USBPort *port = udev->port;
| -+ usb_attach(port);
\ - USBDevice *dev = port->dev;
| - port->ops->attach(port); "usb controller 注册的port, 如usb2.0 ehci"
| - dev->state = USB_STATE_ATTACHED;
| -+ usb_device_handle_attach(dev);
\ - klass->handle_attach(dev); "按设备类型进行, 不一定实现了这个方法"

这里重点说下 port->ops->attach(port) , 这个是由 qemu 模拟的usb controller 注册的port, 这里以usb2.0 的ehci 看下注册过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void usb_ehci_realize(EHCIState *s, DeviceState *dev, Error **errp)
{
for (i = 0; i < s->portnr; i++) {
usb_register_port(&s->bus, &s->ports[i], s, i, &ehci_port_ops,
USB_SPEED_MASK_HIGH);
s->ports[i].dev = 0;
}
}
static USBPortOps ehci_port_ops = {
.attach = ehci_attach,
.detach = ehci_detach,
.child_detach = ehci_child_detach,
.wakeup = ehci_wakeup,
.complete = ehci_async_complete_packet,
};

所以 port->ops->attach(port) 指向了 ehci_port_ops.ehci_attach, 重点看下这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void ehci_attach(USBPort *port)
{
EHCIState *s = port->opaque;
uint32_t *portsc = &s->portsc[port->index];
const char *owner = (*portsc & PORTSC_POWNER) ? "comp" : "ehci";
...
*portsc |= PORTSC_CONNECT;
*portsc |= PORTSC_CSC; //该位表示 bus 上接入usb 设备
-+ ehci_raise_irq(s, USBSTS_PCD);
// 给guest 发送中断, 通知guest usb设备接入了, USBSTS_PCD 为usb的特定中断类型
\ ---+ qemu_set_irq(s->irq, level);
}

static void usb_ehci_sysbus_realize(DeviceState *dev, Error **errp)
{
SysBusDevice *d = SYS_BUS_DEVICE(dev);
EHCISysBusState *i = SYS_BUS_EHCI(dev);
EHCIState *s = &i->ehci;
usb_ehci_realize(s, dev, errp);
sysbus_init_irq(d, &s->irq); "中断的注册在这里"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    memory_region_init_io(&s->mem_opreg, OBJECT(dev), &ehci_mmio_opreg_ops, s,
"operational", s->portscbase);
s->async_bh = qemu_bh_new(ehci_work_bh, s);
static void ehci_opreg_write(void *ptr, hwaddr addr,
uint64_t val, unsigned size) {
...
qemu_bh_schedule(s->async_bh);
}
static void ehci_work_bh(void *opaque) {
-+ ehci_advance_async_state(ehci);
\ -+ ehci_advance_state(EHCIState *ehci, int async)
\ -+ ehci_state_execute(q);
\ -+ ehci_execute
\ -+ usb_handle_packet(p->queue->dev, &p->packet);
\ -+ usb_process_one(p);
\ -+ usb_device_handle_data(dev, p);
\ -+ klass->handle_data(dev, p);
\ -+ usb_host_handle_data(dev, p)
\ - libusb_fill_bulk_transfer()
| - libusb_submit_transfer(r->xfer);
}

从上述过程中可以大概了解到

  1. 传入的usb设备需要先借助libusb 库与host 进行解绑 detach, 此时该usb设备与host就没啥关系了
  2. 与host解绑后, 需要绑定到qemu模拟的usb 控制器上, 由qemu 模拟的usb控制器控制usb设备的行为, usb控制器有MemoryRegion等的设定, 以让 guest 进行io的模拟
  3. 中断相关的仍然是使用的qemu的虚拟的中断控制器进行中断的触发
  4. 数据流中最终起作用的是 usb 设备的ep 端点, 最终数据流通过 libusb_*_transfer 等数据相关的接口以ep参数控制对应的端点进行发送接收数据.